optics-dev / Monocly

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

Add the `ProjectCapture` action to `Focus`. #43

Closed yilinwei closed 3 years ago

yilinwei commented 3 years ago

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.

yilinwei commented 3 years ago

@julien-truffaut, @kenbot

  1. Is the encoding going to be inheritance-based in the end?
  2. Do you think it's worth porting to the current Monocle master (once I've addressed any issues)?
julien-truffaut commented 3 years ago

@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.

yilinwei commented 3 years ago

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.

yilinwei commented 3 years ago

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.

julien-truffaut commented 3 years ago

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

yilinwei commented 3 years ago

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?

kenbot commented 3 years ago

This PR is out of date now