-
Notifications
You must be signed in to change notification settings - Fork 22
Working with monocle and optics
The project contains a class TezosOptics
which:
Provides monocle lenses and additional "optics" for most common access and mutation patterns for Tezos type hierarchies and ADTs
The simplest optic is a Lens
. The raison-d'etre of this is better handling of deeply nested fields in case classes.
The issues shows up whenever you need to read and update a value to get a copy of the original object.
This usually translates to a galore of a.copy(b = a.b.copy(c = a.b.c.copy(...)))
Well, you can imagine the fun!
A lens is a function container that wraps both a getter and a setter for an object field. Such lenses can be then composed with each other, using guess what?... a "composeLens" function. Hence you now can have
val a: A = ... //the top object with a field to B
val c: C = ... //a nested object type within B
val aToCLens = aToBLens composeLens bToCLens //creates a lens A -> C by composing existing (A -> B, B -> C) lenses, like functions!
// which allows you to
val nestedC: C = aToCLens.get(a) //you pass the top-level A and immediately read the nested C
val updatedWithC: A = aToCLens.set(c)(a) //you pass a new value for C and the object A you want to update
Now, it looks like somebody lost control using this stuff and thus you now have a whole family of lens-like things but for complex fields and other kind of "nesting", e.g.
- Optional: a getter/setter for fields whose value might not be there
- Prism: a selector within a sealed trait hierarchy (a.k.a. a Sum type) //E.g. you can only pick the
Some
value of aOption
object. - Traversal: a getter/setter that operates on many value in the data structure (e.g. for collection-like things)
- Iso: a bidirectional lossless conversion between two types
(Jokes apart, it happened that generalising the approach, these additional "entities" where somewhat "discovered")
For each optic type, we agree on a standard naming convention that makes each one easier to identify and meaningfully compose with each other. Assuming a field/subtype named XXX:
- Lens:
onXXX
- Optional:
whenXXX
(still in doubt if this expresses well the concept or if we should stick to onXXX) - Prism :
selectXXX
- Traversal:
acrossXXX
- Iso:
ontoXXX
We also make use of standard instances of optics for common types, e.g.
-
stdLeft
/stdRight
, a Prism that digs into a Left/Right value if it matches - ...
And standard functions to obtain optics based on certain conditions (based on type classes), e.g.
-
each
will provide a Traversal for any type that is Traversable (e.g.List
) -
some
will provide an Optional for an instance of Option, in the way you would expect - ...
There's plenty of existing optics available which often could substitute common functions too, e.g.
- Iso between similar representations: List/Vector, NonEmptyList/NonEmptyChain/OneAnd, ...
- Prisms for numeric down-casting BigDecimal to Int/Long, Byte to Boolean, Double to Float, ...
-
curry
/uncurry
, to convert functions between curried/uncurried form -
flipped
to switch two arguments of a function -
head
to access first element of a non-empty cons-list, or option variant for cons-list -
first/second/third/...
to access the nth element - ... more and more