kenbot / goggles

Pleasant, yet principled Scala optics DSL
MIT License
196 stars 7 forks source link

Polymorphic assignment #17

Open kenbot opened 7 years ago

kenbot commented 7 years ago

Case classes naturally support polymorphic update:

case class Foo[A](foo: A)
val f: Foo[Int] = Foo(3)
>> f: Foo[Int]

f.copy(_.toString)
>> res2: Foo[String]

This should work in Goggles too, as expected, using PSetters and PLenses.

set"$f.foo" ~= (_.toString)
kenbot commented 7 years ago

I really want this feature, but the others (QuickLens, Shapeless, Monocle's @GenLens) don't support it, so it can probably wait until after 1.0.

kenbot commented 7 years ago

Ok this one is interesting.

We can't just spit out a PSetter instance, because the STAB types would have to be known up front, and the B and T depend on what the modifying function does. We would have to generate a method, in the style of monocle.std.option.pSome:

  case class Foo[A](a: A)
  def x[A,B] = PSetter[Foo[A], Foo[B], A, B](f => s => s.copy(a = f(s.a)))

  x.modify(_.toString)(Foo(3)) // Compiles
  // set"${Foo(3)}.$x" ~= (_.toString) // Doesn't compile

Calling modify in the usual Monocle way benefits from type inference in an interesting way:

Unfortunately, we seem to be tripping a limitation of this inference. Goggles generates something like:

set"${Foo(3)}.$x" ~= (_.toString)
// becomes
new MonocleModifyOps(AppliedObject.const(Foo(3)).composeSetter(x)) ~= (_.toString)

which fails compilation:

[error] /Users/ken_scambler/projects/toy/goggles/dsl/src/test/scala/goggles/SetDslSpec.scala:212: The types of consecutive sections don't match.
[error]  found   : SetDslSpec.this.Foo[Nothing]
[error]  required: SetDslSpec.this.Foo[Int] 
[error] 
[error]  Sections  │ Types                    │ Optics 
[error] ───────────┼──────────────────────────┼────────
[error]  ${Foo(3)} │ Foo[Int]                 │        
[error]  .$x       │ Foo[Nothing]  ⇒  Nothing │ Setter 
[error]     set"${Foo(3)}.$x" ~= (_.toString)
[error]     ^

We put Foo(3) in the leftmost position, so that S = Foo[Int] is known from the outset, and can flow left-to-right. From Foo[Int], x knows that A is Int, but does not yet know what B or T are.

However, once this incompletely-typed optic gets passed into the MonocleModifyOps constructor, it seems that it passes a threshold where it doesn't want incomplete things any more. B gets fixed to Nothing, and by the time (_.toString) gets read, it's too late.

Goggles' design is for the '~=' modification operator to be regular Scala, which sits outside the macro. It's probably quite hard to smuggle the type back through.

Possible options:

  1. Don't worry about it. Nobody else supports an easy way to do this either.
  2. Make ~= a macro too, so we can replace it all with generated code that we know type checks. I don't really want to make this more "magic" or opaque, but it might work.
  3. Bring the assignment part inside the string syntax: set"${Foo(3)}.$x ~= ${(_.toString)}". I really don't want to do this; the more we put in actual Scala the easier it is to learn and use. There are no end of novelties we can cram in the syntax.
  4. Plunder the enclosing expression around the macro for extra type information. It is considered bad form for macros to do this; most of the enclosingXXX methods that facilitate this are deprecated. Even if it is possible, it seems uncomfortably close to "reimplementing scalac in the macro"

We should certainly do 1. in the short term; more thought and experimentation might yield better options.

kenbot commented 7 years ago

Needs more research, descoping from 1.1.