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
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'
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))
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
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
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
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
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!