Skip to content

Fix #8007: Add regression and show type class derivation with macros #8011

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

Merged
merged 8 commits into from
Jan 24, 2020

Conversation

biboudis
Copy link
Contributor

@biboudis biboudis commented Jan 15, 2020

This PR adds:

  1. A macro-based implementation of the typeclass derivation example in No Labels found when Summoning Mirror and Passing to a Splice #8007 (Macro_1). The regression test from that issue mixed macro, mirrors and inline definition. The observed result was that MirorredElemLabels returned zero results, but there were two problems with original test.
  2. A macro-based implementation that shows the correct skeleton for Summoned Mirror of Spliced Type is neither Sum nor Product #7974 (Macro_2)
  3. A macro-based implementation of the type-class derived method from the docs (Macro_3)
  4. A doc page on how to make a derived method with macros

@biboudis biboudis marked this pull request as ready for review January 17, 2020 12:30
@biboudis biboudis assigned nicolasstucki and unassigned biboudis Jan 20, 2020
@biboudis
Copy link
Contributor Author

@deusaquilus this works with the high-level API 😍 (I just discovered myself, thanks @nicolasstucki)

   case '{ $m: Mirror.ProductOf[T]{ type MirroredElemLabels = $t } } => {

@deusaquilus
Copy link
Contributor

I see how I can use this to get the labels. Can I use the mirror element $m itself to do

given SomeTypeclass[T] = SomeTypeclass.derived(m)

?

@biboudis
Copy link
Contributor Author

biboudis commented Jan 21, 2020

I assume yes. Since #8007 was about labels, I'll update the docs and merge this.

@biboudis biboudis assigned biboudis and unassigned nicolasstucki Jan 22, 2020
@biboudis biboudis changed the title Fix #8007: Add regression for summonMirror at top-level splice Fix #8007: Add regression and show type class derivation with macros (also documentation) Jan 22, 2020
@biboudis biboudis requested a review from odersky January 22, 2020 16:14
@biboudis biboudis changed the title Fix #8007: Add regression and show type class derivation with macros (also documentation) Fix #8007: Add regression and show type class derivation with macros Jan 22, 2020
@biboudis
Copy link
Contributor Author

I see how I can use this to get the labels. Can I use the mirror element $m itself to do

given SomeTypeclass[T] = SomeTypeclass.derived(m)

?

I updated the PR to demonstrate that in Macro_2.scala.

@biboudis biboudis requested review from milessabin and julienrf and removed request for nicolasstucki January 23, 2020 16:12
@biboudis
Copy link
Contributor Author

@julienrf since we updated the code (mainly Macro 3 that shows the full ported example) can you have one more look in the doc page? Thankssss!

@biboudis
Copy link
Contributor Author

biboudis commented Jan 23, 2020

Yeap. As shown in the test file. I updated the doc to make it more clear.

https://github.com/lampepfl/dotty/pull/8011/files#diff-881e839e7676a957a7223b1233af6efaR41

// answering to a (now) deleted comment 🎉

@deusaquilus
Copy link
Contributor

deusaquilus commented Jan 23, 2020

Also, is it possible to do this kind of thing using summonFrom inside ‘derived’ to summon the mirror?

@biboudis
Copy link
Contributor Author

summonExpr is appropriate for use under the scope of the quoted context. What are the use cases that require the use of summonFrom?

@deusaquilus
Copy link
Contributor

summonFrom can be used the same way as summonExpr to create a typeclass that doesn't require specifying anything as 'derived'. Here's the same thing you just did using summonFrom:

package example.eqalternate

import scala.deriving._
import scala.quoted._
import scala.quoted.matching._
import scala.compiletime.{summonFrom, erasedValue}

trait Eq[T] {
  def eqv(x: T, y: T): Boolean
}

object Eq {
  given Eq[String] {
    def eqv(x: String, y: String) = x == y
  }
  given Eq[Int] {
    def eqv(x: Int, y: Int) = x == y
  }

  def check(elem: Eq[_])(x: Any, y: Any): Boolean =
    elem.asInstanceOf[Eq[Any]].eqv(x, y)

  def iterator[T](p: T) = p.asInstanceOf[Product].productIterator

  def eqSum[T](s: Mirror.SumOf[T], elems: List[Eq[_]]): Eq[T] =
    new Eq[T] {
      def eqv(x: T, y: T): Boolean = {
        val ordx = s.ordinal(x)
        (s.ordinal(y) == ordx) && check(elems(ordx))(x, y)
      }
    }

  def eqProduct[T](p: Mirror.ProductOf[T], elems: List[Eq[_]]): Eq[T] =
    new Eq[T] {
      def eqv(x: T, y: T): Boolean =
        iterator(x).zip(iterator(y)).zip(elems.iterator).forall {
          case ((x, y), elem) => check(elem)(x, y)
        }
    }

  inline def summonInstance[T]: Eq[T] = summonFrom {
    case t: Eq[T] => t
  }

  inline def summonAll[T <: Tuple]: List[Eq[_]] = inline erasedValue[T] match {
    case _: Unit => Nil
    case _: (t *: ts) => summonInstance[t] :: summonAll[ts]
  }

  inline def derived[T]: Eq[T] =
    summonFrom {
      case ev: Mirror.Of[T] =>
        inline ev match {
          case s: Mirror.SumOf[T]     => eqSum(s, summonAll[s.MirroredElemTypes])
          case p: Mirror.ProductOf[T] => eqProduct(p, summonAll[p.MirroredElemTypes])
        }
    }

}

object Macro_4 {
  implicit inline def eqGen[T]: Eq[T] = Eq.derived

  inline def [T](x: =>T) === (y: =>T): Boolean = {
    val eq = summon[Eq[T]]
    eq.eqv(x, y)
  }
}

Then all you have to do this this!

package example.eqalternate

@main def eqTest() = {
  case class Address(street: String) // Hurrah! Don't need to do 'derived'
  case class Person(name: String, address: Address) // Hurrah! Don't need to do 'derived'

  import Macro_4._

  println( Person("Joe", Address("123")) === Person("Joe", Address("123")) )
}

Could you please add this example to the regression test as well?

@biboudis
Copy link
Contributor Author

Indeed, however the point of the prototype and the documentation page was to show that derivation can be implemented with macros as well.

@biboudis biboudis removed the request for review from milessabin January 23, 2020 20:59
@julienrf
Copy link
Contributor

@biboudis I still see the asInstanceOf, it’s not clear to me what you’ve changed except that you also support sum types.

What happens if a data type has a field of type Double or Boolean?

@deusaquilus
Copy link
Contributor

@biboudis My point is that you actually don't need to extend derives or provide given TC[MyClass] instances in order to use typeclass derivation. This is really, really good!... but the documentation doesn't show it at all. Had I known that you can do it with summonFrom, I would never have filed #7875.

@deusaquilus
Copy link
Contributor

I've commented more on this in #7875 which we can treat as a separate issue.

@julienrf
Copy link
Contributor

@deusaquilus IIUC, using derives provides two benefits:

  • derived type class instances are cached (they are not derived again each time the compiler searches for them), which is very important for performance,
  • it also makes things more predictable: if I didn’t write derives JsonCodec, I don’t want my data type to be serializable in JSON.

The schema you propose is also known as “auto” derivation, whereas derives promotes “semi-auto” derivation. (these terms have been coined by Circe if I’m not mistaken)

@deusaquilus
Copy link
Contributor

deusaquilus commented Jan 23, 2020

@julienrf I agree that semi-auto derivation is a very good thing but it should certainly be known to the community that auto-derivation is also possible (and how it do it should be documented...). My impression from the documentation was it's not. If you want I can file a separate issue for this or just rename #7875.

@biboudis
Copy link
Contributor Author

@biboudis I still see the asInstanceOf, it’s not clear to me what you’ve changed except that you also support sum types.

Indeed your encoding is more elegant and avoids the cast. Without it made the code a bit more straightforward into porting it from the inline version (even though the original doesn't use productElement at all).

What happens if a data type has a field of type Double or Boolean?

Indeed the summonAll needs to be exhaustive from a library author's perspective. I figured that for the exposition is enough to show just two. If that was a real implementation I think the summonAll would need to be more sophisticated.

@biboudis
Copy link
Contributor Author

biboudis commented Jan 23, 2020

@deusaquilus IIUC, using derives provides two benefits:

  • derived type class instances are cached (they are not derived again each time the compiler searches for them), which is very important for performance,
  • it also makes things more predictable: if I didn’t write derives JsonCodec, I don’t want my data type to be serializable in JSON.

The schema you propose is also known as “auto” derivation, whereas derives promotes “semi-auto” derivation. (these terms have been coined by Circe if I’m not mistaken)

One problem with the macro-based approach is that since generated/synthesised definitions are not top-level, as a result they are not cached and it is not clear to me how this can be achieved (only with macro) code. @julienrf do you have any ideas regarding this? Otherwise, I'll need to note this somewhere.

@julienrf
Copy link
Contributor

Indeed the summonAll needs to be exhaustive from a library author's perspective.

I guess there is a way to not require this exhaustivity. How to make this extensible? A derived instance of a type class TC for a type A should just try to summon[TC[Elem]] for each type Elem that is “aggregated” in A.

@julienrf
Copy link
Contributor

as a result they are not cached

If the following code:

case class User(name: String, age: Int) derives Show

Desugars to:

case class User(name: String, age: Int) derives Show

object User {
  given as Show[User] = Show.derived
}

Then the derived instance will be cached regardless of whether the derivation mechanism is implemented with macros or summonFrom, no?

@biboudis
Copy link
Contributor Author

biboudis commented Jan 23, 2020

Then the derived instance will be cached regardless of whether the derivation mechanism is implemented with macros or summonFrom, no?

Yes! You are right about the derives. My mistake, I didn't make clear what I meant. I was referring to the macro-based implementation that is included in this PR. This by default cannot provided cached derivations. I was wondering what would we need, to support this mechanism of caching for macro authors (general inquiry).

@julienrf
Copy link
Contributor

Shapeless has a Cached[A] type that ensures that summoning an A will be cached. I’m not sure whether we need something as expilcit (developers have to use Cached[Show[User]] instead of just Show[User]) or if this could be always enabled in the compiler.

@biboudis biboudis requested review from OlivierBlanvillain and removed request for odersky January 24, 2020 08:40
Copy link
Contributor

@OlivierBlanvillain OlivierBlanvillain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in the long run we should move the text of this example and of the main example from derivation.md into their respective pos tests, and point to them from the docs. IMO these doc pages should be written in the style of a specification rather than "this and that by example"...

@biboudis biboudis merged commit 031e6f2 into scala:master Jan 24, 2020
@biboudis biboudis deleted the fix-#8007 branch January 24, 2020 08:51
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.

5 participants