optics-dev / Monocly

Optics experimentation for Dotty
MIT License
32 stars 5 forks source link

Single-use Focus with pre-applied target #24

Closed kenbot closed 2 years ago

kenbot commented 3 years ago

Julien had an idea:

It is probably more common to create an optic and use it only once, especially for a new user of the library. It would be great if we could make the Monocle API so attractive that users would reach out for it if they have 2 nested copies or even the one.

The following syntax could tick all the boxes: foo.focus(_.field1.?.field2.at("x")).get/replace/modify. It would be equivalent to do:

val optic = Focus[Foo](_.field1.?.field2.at("x"))
optic.get(foo)

Another benefit is that you don't have to name the optic. Naming is hard and there is no standard naming convention for optics.

It should be easy enough to spike up.

kenbot commented 3 years ago

Alrighty, the one we're all waiting for!

There are some design decisions to be made. Pre-applying the object essentially sets the S type to Unit, but calling get, getOption, modify and friends with an explicit () is beyond goofy, and a poor UX.

There's a few things we can do.

  1. Easiest is a zoo of ApplyLens, ApplyFold, etc structures like the .applied feature in Monocle 2, which expose T return types instead of S => T. These need to be exposed to users though, we can't hide them in the internal package. It significantly increases the surface area of Monocle's API.

  2. Another thing is to take all the get, getOption, modify, etc, take them out of the optics classes and turn them into extension methods, returning T when S =:= Unit and S => T otherwise. I'm pretty sure there's a thing in Scala 3 that lets you do this kind of type-level "not" logic. I kind of hate this option. It's probably brittle, complex, and way too clever. How do you abstract over optics where you don't know what the S type is? The user-visible compiler errors would probably be confusing garbage.

  3. Another option is to return a one-size-fits all ApplyOptics[Optic[_,_,_,_], T, A, B]] which leaves the regular optics classes alone, but uses extension methods to offer the right subset of get, etc, based on which Optic is selected.

I think I mildly prefer 3 over 1, both well ahead of 2.

What do you reckon @julien-truffaut?

julien-truffaut commented 3 years ago

Yeah I would also move away from 2 and I am not sure 3 would save much boiler plate code.

So even though it is frustrating, I would prefer 1. It is very boring code that we have to write so that users don't need to think about it and have a simple mental model.

kenbot commented 3 years ago

Check this out @julien-truffaut - The applied thing is a very small amount of code per optic class:

  class AppliedLens[S, T, A, B](from: S, underlying: PLens[S, T, A, B]) {
    def get: A = underlying.get(from)
    def replace(to: B): T = underlying.replace(to)(from)
    def modify(f: A => B): T = underlying.modify(f)(from)

    export underlying.{some, adapt, andThen, asTraversal, asOptional}
  }

I'm playing around with making it an actual standard optic feature, rather than a hack for the Focus macro. So in object PLens we have the above class definition, and in class PLens, we have:

  def appliedTo(s: S): PLens.Applied[S, T, A, B] = 
    PLens.Applied(s, this)

And and even easier one for Unit, in the companion object again:

  extension [T, A, B] (lens: PLens[Unit,T,A,B])
    def applied: PLens.Applied[Unit, T, A, B] = 
      PLens.Applied((), lens)

Wdyt?

julien-truffaut commented 3 years ago

That looks fancy! I am not familiar with export, for example, what would be the signature of andThen for AppliedLens? Will it returns a PLens like underlying or an AppliedLens like it does right now in Monocle 2?

kenbot commented 3 years ago

It would return the exact same thing as the underlying, a PLens. We'd have to customise the methods one by one to stay in Appliedland, no free lunch there

julien-truffaut commented 3 years ago

It would return the exact same thing as the underlying, a PLens. We'd have to customise the methods one by one to stay in Appliedland, no free lunch there

Make sense, but then I don't see the benefits of export. We do want to change the return type, no?

kenbot commented 3 years ago

Yep, looking at the Apply stuff in Monocle now, I see it creates this whole parallel universe consistent with the applied value, which makes sense. There's no shortcut for that, so you're right, I'll have to recreate the full thing.

julien-truffaut commented 3 years ago

This duplication is really annoying but I don't see a satisfying alternative.

I don't think we need to recreate the full API here. What you did is more than enough to shows that it works.