estatico / scala-newtype

NewTypes for Scala with no runtime overhead
Apache License 2.0
540 stars 31 forks source link

WIP: Add deriving and derivingK to @newtype #59

Open ybasket opened 4 years ago

ybasket commented 4 years ago

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:

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:

@newtype case class PhoneNumber(value: String)

object PhoneNumber {
  implicit val eq: Eq[PhoneNumber] = deriving
  implicit val show: Show[PhoneNumber] = deriving
  implicit val hash: Hash[PhoneNumber] = deriving
}

becomes

@newtype(deriving[Eq, Show, Hash]) case class PhoneNumber(value: String)

More examples can be found in the tests.

Fristi commented 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)
ybasket commented 4 years ago

@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:

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.

carymrobbins commented 4 years ago

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).