How do I explain Optics?

I am trying to write some introduction/explanation for optics.
Delving into optics to do that, I found it hard to explain the profunctor representation, especially with Traversal. Having hard time finding the typeclass of Profunctor which yields Traversal…
Could anyone help me with this?

I’d also like a great material introducing optics, to reflect my writing on.

A key point IMHO is to distinguish between two viewpoints on optics:

  • for (beginner) users, explaining how to use an optics library, independently of representation choices;
  • for implementers / advanced users, explaining how the various representations work and the implications of choosing e.g. profunctor vs. van Laarhoven.

Which are you trying to explain?

As one of the package authors I’m biased, but you may be interested in the package docs for the optics package. That’s very much focused on the first of these points; the representation choice is treated as an internal implementation detail rather than something users of the library should have to care about.

Delving into optics to do that, I found it hard to explain the profunctor representation, especially with Traversal. Having hard time finding the typeclass of Profunctor which yields Traversal…

For these kinds of question about the profunctor representation I would refer to Profunctor Optics: Modular Data Accessors by Pickering, Gibbons and Wu, which shows a clean way to define profunctor traversals. But unlike the situation with the van Laarhoven representation, for profunctors the relevant typeclasses are not standardised in base, so there is more room for differences in implementation (e.g. optics uses a class that corresponds rather directly to the van Laarhoven representation).

5 Likes

Thank you for detailed directions! I am having problem coming up with a way to explain FunList intuitively. Any ideas? (Tbh, I myself had some problem understanding what it does…)

The paper says

One may verify inductively that FunList A B T is isomorphic to ∃n . A^n × (B^n → T)

which I personally find helpful as an intuition: a FunList A B T is a collection of some number of As, plus a way to turn the same number of Bs into a T. This is just what a Traversal S T A B gives you: a way to extract some number of As from an S, plus a way to put back Bs in their place to get a T. Thus having a Traversal S T A B amounts to showing that S is isomorphic to FunList A B T.

That said, depending on your audience, explaining traversals via concrete datatype representations like FunList may not necessarily be helpful. I suppose if you say that a Lens S T A B is equivalent to S -> (A × (B -> T)) then by analogy it helps to say a traversal is S -> (∃n . A^n × (B^n → T)). But encoding the latter type in Haskell (via FunList or otherwise) doesn’t necessarily help explain what traversals are, nor is it necessarily a good way to implement them.

2 Likes

Thank you! Now that should help a lot!

The package docs for the optics package are indeed a great resource. I finally got around reading up on the theory behind prisms, after having used them for quite a while now.

Is there a recommend/obvious way on how to define my optics when using the optics library? I found generic-optics, which seems useful (I already used generic-lens) and then there is optics-TH.

Would a move from lens to optics affect my choice between template haskell and generic optics? Are there important alternatives?

You can use OverloadedLabels in combination with generics to get lenses/prisms for record fields using only optics-core (the Optics.Label docs have more details). This is a nice benefit of the optics approach.

If those aren’t suitable, both optics-th and generic-optics can be useful, and correspond fairly directly to the TH support in lens, and generic-lens, respectively. The trade-off between TH and generics is a slightly tricky one, though it mostly doesn’t matter that much. TH can produce top-level definitions, but on the other hand can be difficult to use in some contexts (e.g. cross-compilation). They both impose compile-time performance costs but in different ways (TH can force more compilation and slows down recompilation, while generics avoids that but tends to generate large programs that take longer to compile).

1 Like

These docs in Optics.Label address my question perfectly. I was wondering about the state of the art regarding records in Haskell, especially after GHC9 brought along further syntax extensions. I will certainly try out the recommended approach next time.

Are you trying to explain to beginners who want to learn how to use things or for people interested in jumping straight into the theory behind things? The optics docs seem well-written as a reference for people looking up things or wanting to get the theory, but I would never give them to a beginner. Nor would I see the reason for adding an optics dependency from reading the first two screenfuls of that page.

The one thing I liked about Perl was their doc tradition of starting with a SYNOPSIS that just gives a self-contained usage example (oh look perl has lenses). See also Technical Writing: Learning from Kernighan

There was some game-dev advice about “don’t show the player the key before they’ve seen the locked door”. So start with examples that actually solve problems. You haven’t made the world a better place if people start writing aurPkgs ^.. each . field @"version" where map spVersion aurPkgs would suffice =P

2 Likes

Thank you for feedback!

Yes, it was for ppl who know how to use lens, and are interested in jumping into the backing theory. Specifically, for those who wanted to implement an optics library.

To be fair, though, I did include motivating example for lens - for accessing nested structures. Ofc, the problem is that it is not much motivating. Why invent jargon when you could simply mutate inner field without much hassle?

1 Like