The following are appendices from Optics By Example, a comprehensive guide to optics from beginner to advanced! If you like the content below, there's plenty more where that came from; pick up the book!
Operators may look like an earthquake hit the mechanical keyboard factory, but there's actually a bit of a language to the whole thing which starts to make sense after a bit of practice.
Once you get used to the ideas you can usually guess the name of a symbol which does what you need, and it'll usually exist!
Symbol | Description |
---|---|
^ |
Denotes a Getter |
@ |
Include the index with the result |
. |
Get a single value |
.. |
Get a List of values |
? |
Maybe get the first value |
! |
Force a result or throw an exception if missing |
(^@..) :: s -> IndexedFold i s a -> [(i, a)]
: A getter (^
) which includes the index (@
) in a list of all focuses (..
).
"Yarrr" ^@.. folded
[(0,'Y'),(1,'a'),(2,'r'),(3,'r'),(4,'r')]
(^?!) :: s -> Traversal' s a -> a
: A getter (^
) which forcibly gets (!
) a possibly missing (?
) value.
>>> Just "Nemo" ^?! _Just
"Nemo"
>>> Nothing ^?! _Just
*** Exception: (^?!): empty Fold
CallStack (from HasCallStack):
error, called at src/Control/Lens/Fold.hs:1285:28 in lens
E:Control.Lens.Fold
^?!, called at <interactive>:1:1 in interactive:Ghci4
Symbol | Description |
---|---|
. |
Set the focus |
% |
Modify the focus |
~ |
Denotes a Setter/Modifier |
= |
Denotes a Setter/Modifier over a MonadState context |
< |
Include the altered focus with the result |
<< |
Include the unaltered focus with the result |
%% |
Perform a traversal over the focus |
<> |
mappend over the focus |
? |
Wrap in Just before setting |
+ |
Add to the focus |
- |
Subtract from the focus |
* |
Multiply the focus |
// |
Divide the focus |
|| |
Logically or the focus |
&& |
Logically and the focus |
@ |
Pass the index to the modification function |
(<>~) :: Monoid a => Traversal' s a -> a -> s -> s
: A setter (~
) which mappend
s (<>
) a new value to the focus.
>>> ["Polly want a", "Salty as a soup"] & traverse <>~ " cracker!"
["Polly want a cracker!","Salty as a soup cracker!"]
(<<%@=) :: MonadState s m => Traversal s s a b -> (i -> a -> b) -> m a
: Modify (%
) the focus from within a MonadState
(=
), passing the index (@
) to the function as well. Also return unaltered (<<
) original value.
This one's a bit tricky:
>>> runState (itraversed <<%@= \k v -> "item: " <> k <> ", quantity: " <> v) (M.singleton "bananas" "32")
("32",fromList [("bananas","item: bananas, quantity: 32")])
(%%~) :: Traversal s t a b -> (a -> f b) -> s -> f t
: A setter (~
) which traverses (%%
) a function over the focus.
>>> (1, 2) & both %%~ (\n -> if even n then Just n else Nothing)
Nothing
>>> (2, 4) & both %%~ (\n -> if even n then Just n else Nothing)
Just (2,4)
This table is adapted from the documentation of the Scala optics library Monocle. I've simply altered it to match the lens
library.
The value of each cell denotes the most general type you can achieve by composing the column header with the row header.
The type of an optic is determined by collecting all the constraints of all composed optics in a path. Since constraints collection acts as a set union (which is commutative) the order of composition has no effect on the resulting optic type. Therefore the following table is symmetric across its diagonal.
"--" signifies that the optics are incompatible and do not compose.
Fold | Getter | Setter | Traversal | Prism | Lens | Iso | |
---|---|---|---|---|---|---|---|
Fold | Fold | Fold | -- | Fold | Fold | Fold | Fold |
Getter | Fold | Getter | -- | Fold | Fold | Getter | Getter |
Setter | -- | -- | Setter | Setter | Setter | Setter | Setter |
Traversal | Fold | Fold | Setter | Traversal | Traversal | Traversal | Traversal |
Prism | Fold | Fold | Setter | Traversal | Prism | Traversal | Prism |
Lens | Fold | Getter | Setter | Traversal | Traversal | Lens | Lens |
Iso | Fold | Getter | Setter | Traversal | Prism | Lens | Iso |
For example, to determine which type we get by composing traverse
with _Just
we first check each of their types to discover that traverse
is a Traversal
and _Just
is a Prism
. We then look up the column (or row) with the header Traversal
, then find the cell with the corresponding header Prism
on the other axis. Performing this look up we see that the composition traversed . _Just
results in a Traversal
.
The following chart details which optics are valid substitutions for one another.
As an example, let's say we were curious if all Prisms are a valid Traversal; we first find the row with Prism in the first column; then find the corresponding Traversal column and find a Yes
; meaning that a Prism is a valid substitution for a Traversal.
The optic in the first column is the optic you have, the other column headers represent the type you'd like to use it as.
Fold | Getter | Setter | Traversal | Lens | Review | Prism | Iso | |
---|---|---|---|---|---|---|---|---|
Fold | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
Getter | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
Setter | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
Traversal | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
Lens | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Review | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
Prism | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
Iso | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
When reading type-errors or strange optics signatures you can usually guess the type of optic an operation needs by matching the constraints against the following chart.
Remember that most optics take the form:
(a -> f b) -> (s -> f t)
However Iso
s, Prism
s and Review
s generalize over the Profunctor type and thus can also have constraints on p
:
p a (f b) -> p s (f t)
Keep this in mind when reading the chart.
Optic | Constraints |
---|---|
Lens | Functor f |
Fold | Contravariant f, Applicative f |
Traversal | Applicative f |
Setter | Settable f |
Getter | Contravariant f, Functor f |
Iso | Functor f, Profunctor p |
Prism | Applicative f, Choice p |
Review | Settable f, Profunctor p, Bifunctor p |
Composing optics adds constraints together, then running an optic with an action matches a data type which fulfills those constraints. Here's a table of lens actions and the data-type they use to "run" the optics you pass to them:
Action | Data type |
---|---|
view (^.) | Const |
set (.~) | Identity |
over (%~) | Identity |
fold queries: (toListOf, sumOf, lengthOf, etc.) | Const |
review (#) | Tagged |
traverseOf (%%~) | None |
matching | Market |
Thanks for reading! If you learned something, consider getting the rest of the book, this cheat sheet only scratches the surface!
You can get Optics By Example here. Thanks!
Feel free to share this cheat-sheet with your friends.
A
Traversal
cannot be used as aGetter
, otherwise your matrix version of my feature join semilattice looks right.