Open ybasket opened 4 years ago
This can also be achieved more generally:
implicit def coercibleShow[N, P](implicit ev: Coercible[N, P], R: Show[P]): Show[N] =
R.contramap[N](ev.apply)
implicit def coercibleEq[N, P](implicit ev: Coercible[N, P], R: Eq[P]): Eq[N] =
R.contramap[N](ev.apply)
implicit def coercibleOrder[N, P](implicit ev: Coercible[N, P], R: Order[P]): Order[N] =
R.contramap[N](ev.apply)
@Fristi thank you for the feedback. Your examples definitely work well for many use cases, no doubts. There's also the "dual" to derive any type class instance for a given newtype (example taken from the NewTypesMacrosTest
):
@newtype case class Text(private val s: String)
object Text {
implicit def typeclass[T[_]](implicit ev: T[String]): T[Text] = deriving
}
There are use cases though where these two approaches have their drawbacks. The approach you mention has the following:
it is not specific to one newtype. That is especially problematic when you want to define some type class instances yourself (for different behavior) as they'll be ignored if put in the respective companion objects (lexical scope beats implicit scope). Example:
object Demo {
@newtype case class Text(value: String)
object Text {
implicit val greetable: Greetable[Text] = new Greetable[Text] {
override def greet(t: Text): String = s"Text: ${t.value}"
}
}
trait Greetable[T] {
def greet(t: T): String
}
object Greetable {
implicit val string: Greetable[String] = new Greetable[String] {
override def greet(t: String): String = t
}
}
implicit def coercibleGreetable[N, P](implicit ev: Coercible[N, P], R: Greetable[P]): Greetable[N] =
new Greetable[N] {
override def greet(t: N): String = R.greet(t.coerce[P])
}
val greeting = implicitly[Greetable[Text]].greet(Text("Hello!"))
println(greeting) // gives "Hello" instead of "Text: Hello"
}
The deriving
and derivingK
helpers as they're part of this library now offer a way to handle this in a nice way, but using them involves writing boring, mechanic boilerplate which grows with the amount of type classes you need. This PR suggests an opt-in way to have this boilerplate generated by the macro while maintaining full control over all instances available. Maybe it's not worth adding the complexity, but I see a benefit in it.
There's also the scalaz-deriving plugin which does something similar. Example quoted from the readme -
@newtype @deriving(Encoder, Decoder) case class Bar(s: String)
expanding into
@newtype case class Bar(s: String) object Bar { implicit val _deriving_encoder: Encoder[Bar] = deriving implicit val _deriving_decoder: Decoder[Bar] = deriving }
Not saying something like this isn't worthwhile, but an alternative that might be worth looking at.
Also, I noticed that you are limited to deriving 9 instances at a time, you'll probably want to find a way around that (maybe by taking value arguments instead of type arguments).
Allows to have boilerplate for deriving type classes from a reprentation type instance generated by the macro itself so that in many cases users can avoid writing a companion object manually.
Changes:
deriving
andderivingK
parameters to@newtype
annotationNetTypeMacros.generateNewType
The whole approach is inspired by https://github.com/oleg-py/enumeratum-macro. I made it a WIP as I first wanted to get feedback whether this is seen as a useful addition and whether syntax/naming is fine. If so, I'll write some docs as well.
Example:
becomes
More examples can be found in the tests.