2 Optics

2.1 What are optics?

Optics in its most general sense is a full field of study! In a slightly more concrete sense, optics are a family of tools which are interoperable with one another. Lenses, Folds, Traversals, Prisms and Isos are all types of optics which we’ll explore throughout the book! This isn’t a comprehensive list of all optics, in fact new types are still being discovered all the time!

You’ll gain an intuition for what the more general concept of an optic actually is as you learn about each concrete type and begin to understand what they have in common, but to put it in a nutshell: optics are a family of inter-composable combinators for building bidirectional data transformations.

2.2 Strengths

So why do we actually care about these bidirectional transformation things? The short answer is that they solve a lot of very common data-manipulation problems in a composable, performant, and concise way, the long answer is what follows from here to the end of the book!

I’ll take just a moment to expand on a few strengths:

Composition
Each optic focuses on some subset of data, when composing with other optics they can pick up from where the previous optic left off and dive down even further. This means that each optic you learn becomes a member of your growing vocabulary of optics. Just as words in natural language can be strung together to form a sentence which communicates any intent you might want, a sufficiently complete vocabulary of optics can be arranged to effectively manipulate data to achieve your goals.
Separation of concerns
Optics are the abstraction most programmers didn’t know they needed. They allow us to cleanly separate concerns in stronger ways than either of Object-Oriented or Functional-Programming styles allow on their own. Optics allow us to specify which portions of data we wish to work with separately from the operations we wish to perform on them. We could, for example, encode a pre-order traversal of a tree structure, and combine it with a behaviour which prints the elements. We can swap out either of the data-selector or the action without affecting the other. Say goodbye to the Visitor Pattern!
Concision
Although there are many other good reasons to love optics, they have the nice property of being very succinct. Most tasks can be expressed in a single line of code, and in many cases the resulting code even reads like a simple sentence. For instance: sumOf (key "transactions" . values . key "cost" . _Number) will accept a JSON blob, will look into the “transactions” key, will then dive into each of the elements of the array, and in each of those objects will collect the “cost” of the transaction as a number, summing them all up into a total. In a typical imperative language this operation would likely take a few lines of code, or would at the very least require some ugly nested brackets. If we wish to instead take the average of these numbers we need only swap the sumOf action without fussing about with any variables and loops.
Enforcing interface boundaries
Optics can serve as an external interface which remains consistent despite changes to your data layer. They provide an abstraction layer similar to getters & setters which one might write in Java or Python. This allows you to alter your underlying data structures without breaking external consumers. You can even enforce data-consistency invariants when both getting and setting values! This replaces the idea of class-based “getters” and “setters” while also covering significant ground which used to require interfaces.
A principled and mature ecosystem
Optics have been around for long enough now that the ecosystem has ironed out most bugs and performance issues. There are a wide variety of libraries available, and many popular libraries provide optics-based interfaces (e.g. there are optics wrappers around JSON and XML libraries). Optics have a simple universal construction with the surprising benefit that writing an optics combinator does not require a dependency on any optics libraries! This allows us to use optics as a primitive building block or interface across libraries without worrying about large transitive or cyclic dependencies.

Hopefully all of this is sounding “too good to be true”! I assure you that optics can deliver on these promises. I can also guarantee that it’ll take a little work and more than a few terrible, horrible, no good, very bad type errors to get there, but we’re all in this together!

2.3 Weaknesses

Can’t always have your cake and eat it too; here are a few areas where optics aren’t perfect yet:

Type Errors
Most optics libraries (especially lens) can spew out some pretty ugly type errors when something goes wrong. This is one of the most common complaints, however it’s a problem which is not easily solved. A great deal of type-level complexity is required to keep these libraries polymorphic and performant! We’ll approach new types carefully and will address some common mistakes as well as talking about how to read these terrible beasts, so hopefully we can mitigate this one slightly.
Complex Implementation
Most (all?) optics implementations have their fair share of magic (see: complex category theory and/or dirty hacks) going on behind the scenes. To make matters worse, most libraries are implemented in completely different ways! The Scala implementation is different from the Haskell implementation which is different from the Purescript implementation! They all follow a lot of the same theories and encode the same concepts, but a perfect backing implementation hasn’t been discovered yet, though the profunctor encoding is looking pretty good so far. Luckily most libraries provide helpers which abstract over the underlying implementation so you won’t typically need to worry about it.
Vast collection of combinators
This is one of those “weaknesses” you put on your CV that’s actually a strength in disguise. There are a LOT of helpers and combinators provided in most optics libraries, it’s overwhelming at first, but you’ll learn how to search through them and better find the ones you need; and when you can do that effectively it means you’ll usually be able to find a helper for performing almost any optics task! Just have a little patience, and finish reading this book of course!

2.4 Practical optics at a glance

I’ve talked at a high level about how optics help you perform actions over portions of data. Simple actions you can perform involve variants of viewing, modifying or traversing the selected data.

Here are a few examples of varying difficulty and usefulness which represent a few things we’ll see:

-- View nested fields of some record type
>>> view (address . country) person
"Canada"

-- Update portions of immutable data structures
>>> set _3 False ('a', 'b', 'c')
('a', 'b', False)

-- These selectors compose! 
-- We can perform a task over deeply nested subsets of data.
-- Let's sum all numbers wrapped in a 'Left' within the right half of each tuple
>>> sumOf (folded . _2 . _Left) 
      [(True, Left 10), (False, Right "pepperoni"), (True, Left 20)]
30

-- Truncate any stories longer than 10 characters, leaving shorter ones alone.
>>> let stories = 
      ["This one time at band camp", "Nuff said.", "This is a short story"]
>>> over
      (traversed . filtered ((>10) . length))
      (\story -> take 10 story ++ "...")
      stories 
["This one t...","Nuff said.","This is a ..."]

2.5 Impractical optics at a glance

Here are a few of the more arcane and interesting things optics can do. It’s not important that you understand how these work or what they’re doing, they’re just here to help demonstrate the sheer adaptability of optics. Note how each operation is only one line of code! Hopefully they spark a bit of curiosity!

-- Summarize a list of numbers, subtracting the 'Left's, adding the 'Right's!
>>> import Numeric.Lens (negated)
>>> sumOf (folded . beside negated id) [Left 1, Right 10, Left 2, Right 20]
27

-- Capitalize each word in a sentence
>>> "why is a raven like a writing desk" & worded . _head %~ toUpper
"Why Is A Raven Like A Writing Desk"

-- Multiply every Integer by 100 no matter where they are in the structure:
>>> import Data.Data.Lens (biplate)
>>> (Just 3, Left ("hello", [13, 15, 17])) & biplate *~ 100
(Just 300,Left ("hello",[1300,1500,1700]))

-- Reverse the ordering of all even numbers in a sequence.
-- We leave the odd numbers alone!
>>> [1, 2, 3, 4, 5, 6, 7, 8] & partsOf (traversed . filtered even) %~ reverse
[1,8,3,6,5,4,7,2]

-- Sort all the characters in all strings, across word boundaries!
>>> import Data.List (sort)
>>> ("one", "two", "three") & partsOf (each . traversed) %~ sort
("eee","hno","orttw")

-- Flip the 2nd bit of each number
>>> import Data.Bits.Lens (bitAt)
>>> [1, 2, 3, 4] & traversed . bitAt 1 %~ not
[3,0,1,6]

-- Prompt the user with each question in a tuple,
-- then return the tuple with each prompt replaced with the user's input,
>>> let prompts = ( "What is your name?"
                  , "What is your quest?"
                  , "What is your favourite color?"
                  ) 
>>> prompts & each  %%~ (\prompt -> putStrLn prompt >> getLine)
What is your name?
> Sir Galahad
What is your quest?
> To seek the holy grail
What is your favourite color?
> Blue I think?
("Sir Galahad","To seek the holy grail","Blue I think?")

I hope that was a sufficiently strange list of examples to spark some wonder and creativity. These were meant to show the versatility, expressivity, and concision of optics! These examples are contrived and complex of course, but we’ll see some more practical examples as we go on.