Dotty: Implement lens generation macros

Created on 18 Feb 2019  ·  20Comments  ·  Source: lampepfl/dotty

The following macros are used to cut down boilerplate when generating lens.

  • [x] GenLens
  • [x] GenPrism
  • [ ] GenIso

    • [x] 0 field

    • [x] 1 field

    • [ ] n fields (blocked by missing support for whitebox macros)

Links

Thanks to @milessabin for refering the macros to us.

metaprogramming

Most helpful comment

You can write

Yeah, but that's really quite awkward in this sort of context (which is really quite common). It's a shame ... we ought to be able to do better.

This works with poly functions,

trait Lens[S, T]

def GenLens[S] = [T] -> (get: S => T) => new Lens[S, T] {}

case class Foo(i: Int)

object Test {
  GenLens[Foo](_.i)
}

But I think it would be nicer if we could support multiple type parameter lists, ie.,

def GenLens[S][T](get: S => T) = new Lens[S, T] {}

All 20 comments

Thanks for looking into this! Another macro I wanted to implement but never got the time is GenLens equivalent for common fields of Coproduct e.g.

sealed trait Foo
case class Foo1(name: String, i: Int) extends Foo
case class Foo2(name: String, b: Boolean) extends Foo

val name = GenLens[Foo](_.name)

but it would fail if they were a 3 implementation of Foo like:

case class Foo3(x: Int) extends Foo

I'm pretty sure the answer is "yes", but can you confirm for me that this generalizes to nested selectors, ie. _.address.street?

good question, it would be awesome if it was.

It's pattern matching on the Tasty trees, so I _think_ it could be generalized and Just Work, but it'd be nice to be sure.

@julien-truffaut There is some difficulty to implement the Coproduct lens in Dotty macros:

Dotty macros should type check without expansion.

Even for the non-Coproduct GenLens macro, programmers have to write GenLens[Foo, String](_.name) in Dotty instead of GenLens[Foo](_.name) in Scala2.

Nested selectors _.address.street should be able to be supported.

Could you get it to typecheck by having the target type inferred as Nothing in the unexpanded context?

@milessabin The problem is that given the following macro signature:

object GenLens {
    inline def apply[S, T](get: S => T): Lens[S, T] = ~impl('(get))
}

Dotty does not support only supplying S:

5 |    val len = GenLens[Address](_.streetNumber)
  |              ^^^^^^^^^^^^^^^^^
  |              Not enough type arguments for GenLens.apply[S, T]
  |              expected: [S, T]
  |              actual:   [Address]

@nicolasstucki Any thoughts about this?

In vanilla Scala the usual trick is something like,

object GenLens {
  def apply[S] = new MkGenLens[S]
  class MkGenLens[S] {
    def apply[T](get: S => T): Lens[S, T] = ...
  }
}

Then S can be explicit and T inferred. Could you do something like that here?

@milessabin Thanks a lot for sharing the trick, it works like a charm 👍 73524a7c1ae1367114b8188ecf08fbcb07313790

I thought Dotty was supposed to solve those issues

On Mon, Feb 18, 2019, 4:15 PM Fengyun Liu notifications@github.com wrote:

@milessabin https://github.com/milessabin Thanks a lot for sharing the
trick, it works like a charm 👍 73524a7
https://github.com/lampepfl/dotty/commit/73524a7c1ae1367114b8188ecf08fbcb07313790


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/lampepfl/dotty/issues/5941#issuecomment-464881673,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAGAUJm3K3LFbyhjTf6kcNGbY5pwlETaks5vOxftgaJpZM4bAlx5
.

Dotty does not support only supplying S:

You can write:

val len = GenLens[S=Address](_.streetNumber)

cf http://dotty.epfl.ch/docs/reference/other-new-features/named-typeargs.html

You can write

Yeah, but that's really quite awkward in this sort of context (which is really quite common). It's a shame ... we ought to be able to do better.

This works with poly functions,

trait Lens[S, T]

def GenLens[S] = [T] -> (get: S => T) => new Lens[S, T] {}

case class Foo(i: Int)

object Test {
  GenLens[Foo](_.i)
}

But I think it would be nicer if we could support multiple type parameter lists, ie.,

def GenLens[S][T](get: S => T) = new Lens[S, T] {}

Make a SIP :).

Make a SIP :)

With a PR attached ;-)

The GenPrism and GenLens macros are implemented in #5944.

Unfortunately, we are unable to implement GenIso.fields due to missing support for whitebox macros.

Code examples:

    val len2 = GenLens[Employee](_.addr.streetNumber)
    val employee = Employee("Bob", Address(10, "High Street"))
    assert(len2.get(employee) == 10)
    val employee2 = len2.set(5, employee)
    assert(employee2.name == "Bob")
    assert(len2.get(employee2) == 5)

    val jNum: Prism[Json, Double] = GenPrism[Json, JNum] composeIso GenIso[JNum, Double]
    assert(jNum(3.5) == JNum(3.5))
    assert(jNum.getOption(JNum(3.5)) == Some(3.5))
    assert(jNum.getOption(JNull) == None)

The implementation of GenIso[JNum, Double] is supposed to be more robust than the Scala2 implementation, as it handles the case where the case classes are inner classes.

@liufengyun the work I'm doing on generics will cover GenIso.

It would be nice to implement GenIso.fields based on @milessabin 's work. Could you please let us know how to do that once your work is merged @milessabin ?

I started the implementation of Monocle for Dotty in the following repo: https://github.com/julien-truffaut/Monocly

It would be amazing if any of you would like to participate or point me to some documentation. I tried to adapt the macro in https://github.com/lampepfl/dotty/blob/master/tests/run-macros/i5941/macro_1.scala but I am running into some issues. It is probably because I am using polymorphic optics (4 or 5 type parameters) and a curried set method.

@julien-truffaut The following is the latest prototype implementation in Dotty https://github.com/lampepfl/dotty/blob/master/tests/run-macros/i5941/macro_1.scala . The documentation and the paper may also be useful.

In your prototype, I think you should remove the toolbox line, as they are not intended for macros.

  def impl[A: Type, B: Type](getter: Expr[A => B])(given qctx: QuoteContext): Expr[Lens[A, B]] = {
    implicit val toolbox: scala.quoted.staging.Toolbox = scala.quoted.staging.Toolbox.make(this.getClass.getClassLoader)
    import qctx.tasty.{_, given}
Was this page helpful?
0 / 5 - 0 ratings