Closed yilinwei closed 3 years ago
@julien-truffaut, @kenbot
@yilinwei Ideally we will move to inheritance, but we need to fix this issue first: https://github.com/optics-dev/Monocle/issues/996
I am not sure when we will have the time 3.0, 3.1, or later.
Thanks for commenting @kenbot; it's clear that I didn't explain myself enough when writing the original comment. I'll divide the reply to address both of your questions.
Motivation
The biggest motivating factor for me is where a coproduct diverges, but converges back onto a single point. Common fields are certainly an example, but project
is meant to cover 2 separate examples:
The first example is where the types are sufficiently different (i.e. not part of the same inheritance hierarchy). Notice the each
call in the MultiMessage
branch.
sealed trait Message
case class MultiMessage(ids: List[Int]) extends Message
case class SingleMessage(id: Int) extends Message
Focus[Message] {
_.project {
case MultiMessage(ids) => ids.each
case SingleMessage(id) => id
}
}
A second example is where one is nested deeply in another.
case class Envelope(wrapped: SingleMessage)
sealed trait Message
case class SingleMessage(id: Int) extends Message
case class WrappedWrappedMessage(wrapped: Envelope) extends Message
Focus[Message] {
_.project {
case SingleMessage(id) => id
case WrappedWrappedMessage(wrapped) => wrapped.wrapped.id
}
}
The final (more contentious one) is such that users don't need to expose a field at the trait level. Parametric reasoning is quite useful - and I don't typically like to add more on a trait than needs be. I recognize that it's of lesser importance than the other two use-cases though.
As to how common these scenarios are; I'm not entirely sure. Personally I've hit into them enough times to notice them; on the other hand I work disproportionately with external API's and data-munging so there is likely some bias there.
Embed
embed
/andThen
certainly can be put into a separate PR; however, I did not think that it would be worth adding an extra keyword for the minor benefit of composing optics with Focus in the fashion of _.andThen(optic)
. I wanted to show it off as a primary means to allow users to create their own keyword as a first-class citizen to the Focus
macro.
In that regard, ProjectCapture
was meant to be an example of such a keyword. What's happening here, is ProjectCapture
is expanding to the andThen
form; the core part of Focus doesn't know about it at all! Technically speaking, as
, some
, each
could all be theoretically written by the user (with a small amount of work) by expanding into andThen! (as I said earlier, I don't think we'll use it for anything but recursive macros, because the error messages are worse).
EmbeddedOptic
We could have overrides for now, for each optic which would get rid of the case class
. It would mean changing to a different implementation when inheritance drops, but I don't think that's too important.
As a final note - what I've noticed from teaching Monocle is that dealing with co-products where both branches are of equal importance (so not a simple downcast) is where people start having to compose optics by calling get
/replace
manually.
I would like to push that down the line as far as possible.
I have the impression that orElse
would be a better option for the two examples (less magical):
Focus[Message](_.as[MultiMessage].ids.each).orElse(
Focus[Message](_.as[SingleMessage].id)
)
Focus[Message](_.as[SingleMessage].id).orElse(
Focus[Message](_.as[WrappedWrappedMessage].wrapped.wrapped.id)
)
Edit: actually it only works for Optional
not Traversal
, so it would not work for example 1
I believe that the intuition is eas(ier) to teach because it looks like a match arm and hence there is no need to introduce new syntax to the user. IMO, this precisely why the focus macro is useful in the first place, it repurposes the syntax of dot notation which people are already familiar with, the semantics of field access.
In the same way, project is meant to reuse the syntax of match case with the semantics of field access as well.
Perhaps calling it _match
would make it more easy to parse?
This PR is out of date now
Closes #14.
I had a few spare hours so I knocked up the concept in #14; embarrassingly I forgot which encoding of optics we were using so I coded the whole thing assuming inheritance so it doesn't work in the case that we mix the optics currently. I did wonder wonder why dotty wouldn't resolve certain properties but since it's fiddly anyway, I'd assumed that it was just "one of those things".
At any rate, this is a sketch of a
project
action which allows users to work with co-products. The second part, which I'm terribly excited about, is the other concept mentioned in the issue; the concept of opening up the DSL.In particular the
case class EmbeddedOptic
allows users to arbitrary embed constructed optics into the Focus DSL. Currently the 3 types don't align correctly (which is ofc because of the inheritance issues which we were talking about earlier), but I think the gist of the solution should be clear.In general I don't think we'll be using this mechanism much ourselves (apart from in the case we want a second-phased macro expansion like in project), but it's extremely nice that the
Focus
becomes fully composable with "ordinary" optics.