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