Closed kenbot closed 2 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.
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.
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.
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?
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.
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?
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?
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 Applied
land, no free lunch there
It would return the exact same thing as the underlying, a
PLens
. We'd have to customise the methods one by one to stay inApplied
land, 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?
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.
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.
Julien had an idea:
It should be easy enough to spike up.