Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add deriving and derivingK to @newtype #59

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

ybasket
Copy link

@ybasket ybasket commented Mar 3, 2020

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:

  • add deriving and derivingK parameters to @newtype annotation
  • generate implicit companion object members for each mentioned type class
  • remove two unused lines in NetTypeMacros.generateNewType
  • Update cats to stable version

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.

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:
* add deriving and derivingK parameters to @newtype annotation
* generate implicit companion object members for each mentioned type class
* remove two unused lines in NetTypeMacros.generateNewType
* Update cats to stable version
@Fristi
Copy link

Fristi commented Mar 4, 2020

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
Copy link
Author

ybasket commented Mar 7, 2020

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

  • definition outside of the implicit scope, forcing users of your newtypes to import them
  • 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.

@carymrobbins
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants