goggles icon indicating copy to clipboard operation
goggles copied to clipboard

Polymorphic assignment

Open kenbot opened this issue 8 years ago • 3 comments

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 avatar Jan 24 '17 23:01 kenbot

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 avatar Feb 15 '17 12:02 kenbot

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:

  • A and B are unknown at the start of the expression
  • (_.toString) doesn't have enough information to know what _ is yet
  • Foo(3) gives the game away; S = Foo[Int], therefore A = Int, and the toString bit means that B = String, and finally T = Foo[String].
  • The types flow backwards from right to left, as if the whole expression knew the types all along.

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 avatar Apr 22 '17 05:04 kenbot

Needs more research, descoping from 1.1.

kenbot avatar May 18 '17 03:05 kenbot