Monocle 3.x
We recently started the development of the next major version of Monocle, Monocle 3.x. In this post, I would like to explain our objectives and discuss some of the changes we intend to make.
Monocle 3.x will be a complete rewrite. It doesn't mean we will change everything, but we will question every aspect of the library: optics encoding, API, names, dependencies, etc. We defined the following objectives to help us make trade-offs in the new design:
- User-friendly interface. A user should be able to perform the most common actions without requiring in-depth knowledge of optics.
- Correctness. Optics follow certain essential principles. Those rules may not be intuitive, but without them, optics are not a useful abstraction. The API should make it easy to follow those principles and avoid any undesired behaviours.
- Focus on Scala 3. The next major version of Scala is coming soon, we should design the API firstly with Scala 3 in mind.
- Performance. Optics are slower than handwritten equivalent code. Nevertheless, we should aim to limit this performance hit as much as possible.
Here are some of the specific changes we have in mind. Everything is still on the table; please voice your opinion in Monocle gitter or issue tracker.
Inheritance between optics
There exists a fundamental inheritance relationship between optics. An Iso
is a valid Lens
. A Lens
is a legitimate
Optional
, and so on. The following diagram summarises the optics hierarchy in Monocle.
Currently, in Monocle, optics do not extend one another. Instead, we provide asX
method to manually upcast optics which
is inconvenient for end-users.
|
|
The first version of Monocle used inheritance; this was in 2014! However, we encountered some issues with compose
that
motivated us to "temporarily" remove inheritance. Retrospectively, it was a mistake. We should not have favoured ease of
implementation over ease of utilisation.
Object API for most common optics composition
Composing optics is very similar to composing functions; you need to make the type match, e.g. composing an Optic[A, B]
and an Optic[B, C]
, gives you an Optic[A, C]
. Since optics inherit from one another, we can often compose two different
types of optics; the resulting optic type will be the least upper bound (LUB) of the two types. For example, if you compose
a Lens[A, B]
with a Prism[B, C]
, you get an Optional[A, C]
because Optional
is the first common parent of Lens
and
Prism
(see full table).
In practice, some of the transformations are extremely common. Say we have a Lens[Person, Option[Email]]
, and we want to focus
into the Some
part of the Option
or we have a Lens[Invoice, List[Item]]
, and we want to go a particular index in the list.
For those common operations, we can provide a dedicated method on all optics:
|
|
This feature should make the API more IDE friendly and reduce the learning curve for new users.
Use variance in optic's type parameters #771
A Getter[A, B]
is equivalent to A => B
. So the variance of Getter
should be the same as =>
, contravariant in A,
the input and covariant in B, the output. If you are like me and have trouble putting your head around variance, you
will find a great resource in Thinking with types.
|
|
On the other hand, a Lens[A, B]
is equivalent to a pair of function (get: A => B, set: (A, B) => A)
. Both A and B appear
in covariant and contravariant positions, which means Lens
must be invariant in A and B.
|
|
Similarly, all write optics are invariant, but it turns out that their polymorphic cousin is not. For example, a PolyLens[A1, A2, B1, B2]
is a pair of function (get: A1 => B1, set: (A1, B2) => A2)
. Now, both A1 and B2 are in contravariant position and A2
and B1 are in covariant position.
|
|
Thank you, Adam Fraser and John De Goes for the idea. Also, thanks to Georgi Krastev for pointing out monomorphic
Lens
can inherit Getter
with variance.
Regrettably, polymorphic optics bring other issues. So it is unclear if we will keep them in 3.x or if Monocle will only support monomorphic optics.
0 dependency core
A project with no dependency present several advantages:
- Modularity. End-users may want a better syntax for modifying case classes without adding cats to their code base.
- Independent release cycle. We don't need to release a new version every time an upstream dependency is upgraded.
- More flexibility. We can experiment more easily with plugins and language features (e.g. Scala 3) without waiting for dependencies to support them.
- Smaller footprint. It is particularly significant for other platforms like Scala.js.
Monocle core module only depends on a one functional library, scalaz in 1.x, cats
in 2.x. Monocle exposes a couple of functions using typeclasses like modifyF
, Prism.below
, or Traversal.fromTraverse
. Still,
the main reason for this dependency is the encoding of Traversal
.
We use the Van Laarhoven encoding for Traversal
, which means we define all functions within Traversal
in terms of modifyF
.
|
|
It is still unclear if we can find an alternative encoding of Traversal
without a dependency on cats
(see issue). Assuming we find a suitable encoding one, we can
then create a cats interop module where we would define all cats specific methods and instances. Unfortunately, end-users
will need an additional import to access those functionality. We need to evaluate how often this extra import will be required.
Rename all compose methods to andThen #768
Optics and functions compose in the same way; you need to make the type match a bit like in a puzzle.
|
|
You may have noticed that the parameters of optics composition are the inverse of function composition. Optics composition
looks more like andThen
than compose
.
|
|
Therefore, I propose to rename all the compose
methods in optics to andThen
:
|
|
For those curious about the abstraction behind function and optics, it is called Category.
We can also use the "standard" symbolic alias for andThen
, >>>
(see Compose).
|
|
Monocle needs your help
We are always looking for new contributors and active maintainers of Monocle. You don't need to be an expert in optics; we need more people to comment on our issue tracker. We created a label beginner-friendly for issues suitable for beginners. I am also available to mentor you if you want to dive in the optics world, don't hesitate to reach out to me on twitter or gitter!