Create a gist now

Instantly share code, notes, and snippets.

Lens Cookbook

Optic Cookbook

The imports for building the various field-oriented optics are pretty minimal. It's not until you make a Getter or a Fold that you need to look outside of base.

This cookbook only covers the field oriented optics and not the constructor oriented ones. If you want to build a Prism or an Iso without a lens dependency, you should copy the definition of lens' prism and iso combinators and add a profunctors dependency to your project. Those two combinators are quite self-contained.

-- For Lenses
-- fmap is imported by default!

-- For Traversals
import Control.Applicative (Applicative((<*>),pure),(<$>))

-- For Getters and Folds
import Data.Functor.Contravariant (Contravariant(contramap))

Simple Optics

Building a Lens for a product type. The keys to these is that while you can cover more than one constructor, every constructor case will need to apply your function f to a single value.

data T1 = C1 Int Char Bool

t1Int :: Functor f => (Int -> f Int) -> T1 -> f T1
t1Int f (C1 x y z) = fmap (\x' -> C1 x' y z) (f x)

t1Char :: Functor f => (Char -> f Char) -> T1 -> f T1
t1Char f (C1 x y z) = fmap (\y' -> C1 x y' z) (f y)

t1Bool :: Functor f => (Bool -> f Bool) -> T1 -> f T1
t1Bool f (C1 x y z) = fmap (\z' -> C1 x y z') (f z)

Building a Traversal for a single field in a sum type. Once you add the Applicative constraint you are free to have constructor cases which handle zero (using pure) or more than one field (using <*>).

data T2 = C2a Int Char | C2b Bool

t2Char :: Applicative f => (Int -> f Int) -> T2 -> f T2
t2Char f (C2a x y) = fmap (\x' -> C2a x' y) (f x)
t2Char _ s@(C2b _) = pure s

Building a Traversal for multiple fields in a single constructor.

data T3 = C3 Int Int Int

t3Int :: Applicative f => (Int -> f Int) -> T3 -> f T3
t3Int f (C3 x y z) = C3 <$> f x <*> f y <*> f z

Before we can build Getters and Folds we need a helper function. This is available as Control.Lens.coerce but we want to define everything ourselves here. This will require a dependency on contravariant. A Getter will need to collect exactly one field from each constructor while a Fold is free to visit zero or many fields in the same manner as a Traversal.

coerce :: (Contravariant f, Functor f) => f a -> f b
coerce = contramap (const ()) . fmap (const ())

Building a Getter for a single field. This isn't an interesting example, but it shows you what the definition looks like.

data T4 = C4 Int Char Bool

t4IntGetter :: (Contravariant f, Functor f) => (Int -> f Int) -> T4 -> f T4
t4IntGetter f (C4 x _ _) = coerce (f x)

Building a Fold for all the Ints in a sum type. You should actually make a Traversal in this case (because you can), but this is just an example.

data T5 = C5a Int Int | C5b Int | C5c

t5IntFold :: (Contravariant f, Applicative f) => (Int -> f Int) -> T5 -> f T5
t5IntFold f (C5a x y) = coerce (f x) <*> f y
t5IntFold f (C5b x  ) = coerce (f x)
t5IntFold _ C5c       = pure C5c

Type-changing Optics

Building a type-changing Lens. Notice that you can only change types when you visit all of the fields that mention a type variable. The t6_1 example only visits one of the two a typed fields, so its type can't change. The t6_3 example visits the lone b typed field, so it's able to replace that value with a different type.

data T6 a b = C6 a a b

t6_1 :: Functor f => (a -> f a) -> T6 a b -> f (T6 a b)
t6_1 f (C6 x y z) = fmap (\x' -> C6 x' y z) (f x)

t6_3 :: Functor f => (b -> f c) -> T6 a b -> f (T6 a c)
t6_3 f (C6 x y z) = fmap (\z' -> C6 x y z') (f z)

The patterns for all of these are the same syntactically as the simple versions above. If you're having trouble figuring out what type to give your optic, just ask GHC!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment