Skip to content

Implement lens generation macros #5941

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

Closed
4 of 6 tasks
liufengyun opened this issue Feb 18, 2019 · 20 comments
Closed
4 of 6 tasks

Implement lens generation macros #5941

liufengyun opened this issue Feb 18, 2019 · 20 comments
Assignees

Comments

@liufengyun
Copy link
Contributor

liufengyun commented Feb 18, 2019

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

  • GenLens
  • GenPrism
  • GenIso
    • 0 field
    • 1 field
    • n fields (blocked by missing support for whitebox macros)

Links

Thanks to @milessabin for refering the macros to us.

@liufengyun liufengyun self-assigned this Feb 18, 2019
liufengyun added a commit to dotty-staging/dotty that referenced this issue Feb 18, 2019
@julien-truffaut
Copy link

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

@milessabin
Copy link
Contributor

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

@julien-truffaut
Copy link

good question, it would be awesome if it was.

@milessabin
Copy link
Contributor

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.

@liufengyun
Copy link
Contributor Author

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

@milessabin
Copy link
Contributor

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

@liufengyun
Copy link
Contributor Author

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

@liufengyun
Copy link
Contributor Author

@nicolasstucki Any thoughts about this?

liufengyun added a commit to dotty-staging/dotty that referenced this issue Feb 18, 2019
@milessabin
Copy link
Contributor

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?

@liufengyun
Copy link
Contributor Author

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

@nafg
Copy link

nafg commented Feb 18, 2019 via email

@smarter
Copy link
Member

smarter commented Feb 19, 2019

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

@milessabin
Copy link
Contributor

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] {}

@smarter
Copy link
Member

smarter commented Feb 19, 2019

Make a SIP :).

@milessabin
Copy link
Contributor

Make a SIP :)

With a PR attached ;-)

liufengyun added a commit to dotty-staging/dotty that referenced this issue Feb 21, 2019
liufengyun added a commit to dotty-staging/dotty that referenced this issue Feb 21, 2019
@liufengyun
Copy link
Contributor Author

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.

@milessabin
Copy link
Contributor

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

liufengyun added a commit to dotty-staging/dotty that referenced this issue Feb 25, 2019
@liufengyun
Copy link
Contributor Author

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 ?

@julien-truffaut
Copy link

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.

@liufengyun
Copy link
Contributor Author

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

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

No branches or pull requests

5 participants