
In Part 2, we built intuition for profunctors: contravariant in the first type parameter, covariant in the second. Part 3 covers the formal API, its laws, and the extensions that make profunctors useful for optics.
The Profunctor Typeclass
A profunctor is a type constructor with two type parameters that supports dimap:
class Profunctor p where
dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
lmap :: (a -> b) -> p b c -> p a c
rmap :: (c -> d) -> p b c -> p b dThe dimap operation transforms both ends:
- First argument
a -> btransforms the input (contravariant: noticebtoain the result) - Second argument
c -> dtransforms the output (covariant:ctodin the result)
The lmap and rmap operations transform one end at a time:
lmap f = dimap f id -- transform input only
rmap g = dimap id g -- transform output onlyMinimal complete definition: either dimap alone, or both lmap and rmap.
The Function Instance
The simplest profunctor is the function arrow:
instance Profunctor (->) where
dimap f g h = g . h . f
lmap f h = h . f
rmap g h = g . hGiven h :: b -> c:
lmap f hpre-composes withf, yielding =h . f- a -> c=
rmap g hpost-composes withg, yielding =g . h- b -> d=
dimap f g hdoes both: =g . h . f- a -> d=
Profunctor Laws
Profunctor instances must satisfy two laws:
Identity Law
Mapping with identity functions changes nothing:
dimap id id = idComposition Law
Sequential dimap calls fuse into one:
dimap f g . dimap f' g' = dimap (f' . f) (g . g')These laws constrain dimap to behave as expected. The identity law says the profunctor structure is preserved when no transformation occurs. The composition law says the order of applying transformations matches function composition.
Beyond Functions: Other Profunctors
The function arrow is the simplest profunctor, but Part 2's Codec example hints at richer structures. A codec's decode operation can fail, returning Either Error b rather than just b. This pattern appears throughout the paired operations from Part 1: parsing can fail, decryption can fail, deserialization can fail.
The profunctors below capture these patterns. Each emphasizes a different aspect of bidirectional transformation:
Tagged (Phantom Input)
A value that ignores its first type parameter:
newtype Tagged a b = Tagged { unTagged :: b }
instance Profunctor Tagged where
dimap _ g (Tagged b) = Tagged (g b)Tagged is covariant in b and ignores a. The lmap operation has no effect since there is no a value to transform.
Star (Kleisli Arrow)
A function returning a functor:
newtype Star f a b = Star { runStar :: a -> f b }
instance Functor f => Profunctor (Star f) where
dimap f g (Star h) = Star (fmap g . h . f)Star wraps a -> f b. With f = Maybe, this models partial functions. With f = IO, this models effectful computations. With f = Either Error, this models the fallible decode side of a codec from Part 2.
Forget (Extract a Value)
A function that extracts a value, ignoring the second type parameter:
newtype Forget r a b = Forget { runForget :: a -> r }
instance Profunctor (Forget r) where
dimap f _ (Forget h) = Forget (h . f)Forget r is contravariant in a and ignores b. This is useful for extracting values from structures without building new ones.
Strong Profunctors
A Strong profunctor can lift through products (tuples):
class Profunctor p => Strong p where
first' :: p a b -> p (a, c) (b, c)
second' :: p a b -> p (c, a) (c, b)Given a profunctor that transforms a to b, Strong lets you transform (a, c) to (b, c), leaving the c component untouched.
The function arrow is Strong:
instance Strong (->) where
first' f (a, c) = (f a, c)
second' f (c, a) = (c, f a)Strong Laws
-- first' and second' are symmetric
first' = dimap swap swap . second'
where swap (a, b) = (b, a)
-- Nesting first' is associative
first' . first' = dimap assoc assoc' . first'
where
assoc ((a, b), c) = (a, (b, c))
assoc' (a, (b, c)) = ((a, b), c)
-- first' commutes with lmap
lmap (first f) . first' = rmap (first f) . first'
where first f (a, c) = (f a, c)Why Strong Matters
Strong profunctors can focus on part of a product while preserving the rest. This is the key capability for lenses:
type Lens s t a b = forall p. Strong p => p a b -> p s tA Lens works for any Strong profunctor. It focuses on the a inside an s, transforming it to b and yielding a t.
Choice Profunctors
A Choice profunctor can lift through sums (Either):
class Profunctor p => Choice p where
left' :: p a b -> p (Either a c) (Either b c)
right' :: p a b -> p (Either c a) (Either c b)Given a profunctor that transforms a to b, Choice lets you transform Either a c to Either b c, leaving the c branch untouched.
The function arrow is Choice:
instance Choice (->) where
left' f = either (Left . f) Right
right' f = either Left (Right . f)Choice Laws
-- left' and right' are symmetric
left' = dimap mirror mirror . right'
where mirror = either Right Left
-- Nesting left' is associative
left' . left' = dimap assoc assoc' . left'
where
assoc (Left (Left a)) = Left a
assoc (Left (Right b)) = Right (Left b)
assoc (Right c) = Right (Right c)
assoc' (Left a) = Left (Left a)
assoc' (Right (Left b)) = Left (Right b)
assoc' (Right (Right c))= Right c
-- left' commutes with lmap
lmap (left f) . left' = rmap (left f) . left'
where left f = either (Left . f) RightWhy Choice Matters
Choice profunctors can target one branch of a sum while passing through others. This is the key capability for prisms:
type Prism s t a b = forall p. Choice p => p a b -> p s tA Prism works for any Choice profunctor. It targets the a case inside an s, transforming it to b when present.
Combining Strong and Choice
Some profunctors are both Strong and Choice. The function arrow is one:
-- (->) is Strong
first' f (a, c) = (f a, c)
-- (->) is also Choice
left' f = either (Left . f) RightProfunctors with both capabilities can express more optics:
| Optic | Constraint | Focus |
|---|---|---|
| Lens | Strong | Part of a product |
| Prism | Choice | Branch of a sum |
| Affine | Strong + Choice | Optional part of a product |
Full Code Listing
{-# LANGUAGE RankNTypes #-}
module Profunctors where
-- Core Profunctor
class Profunctor p where
dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
lmap :: (a -> b) -> p b c -> p a c
rmap :: (c -> d) -> p b c -> p b d
lmap f = dimap f id
rmap g = dimap id g
dimap f g = lmap f . rmap g
instance Profunctor (->) where
dimap f g h = g . h . f
-- Strong: products
class Profunctor p => Strong p where
first' :: p a b -> p (a, c) (b, c)
second' :: p a b -> p (c, a) (c, b)
instance Strong (->) where
first' f (a, c) = (f a, c)
second' f (c, a) = (c, f a)
-- Choice: sums
class Profunctor p => Choice p where
left' :: p a b -> p (Either a c) (Either b c)
right' :: p a b -> p (Either c a) (Either c b)
instance Choice (->) where
left' f = either (Left . f) Right
right' f = either Left (Right . f)
-- Optic type aliases
type Lens s t a b = forall p. Strong p => p a b -> p s t
type Prism s t a b = forall p. Choice p => p a b -> p s t
-- Example profunctors
newtype Star f a b = Star { runStar :: a -> f b }
instance Functor f => Profunctor (Star f) where
dimap f g (Star h) = Star (fmap g . h . f)
instance Functor f => Strong (Star f) where
first' (Star f) = Star (\(a, c) -> fmap (\b -> (b, c)) (f a))
second' (Star f) = Star (\(c, a) -> fmap (\b -> (c, b)) (f a))
newtype Forget r a b = Forget { runForget :: a -> r }
instance Profunctor (Forget r) where
dimap f _ (Forget h) = Forget (h . f)
instance Monoid r => Choice (Forget r) where
left' (Forget f) = Forget (either f (const mempty))
right' (Forget f) = Forget (either (const mempty) f)Summary
The Profunctor API provides:
dimap: Transform both input and outputlmap/rmap: Transform one sideStrong: Lift through products (tuples)Choice: Lift through sums (Either)
These primitives compose to build optics, codecs, and other bidirectional abstractions. The laws guarantee consistent behavior, and the type constraints (Strong, Choice) document what capabilities each abstraction requires.
The profunctor approach separates the what (the transformation) from the how (the specific profunctor instance). Library code written against Profunctor, Strong, or Choice works with any conforming type, enabling reuse and composition.
The paired operations from Part 1 (encode/decode, serialize/deserialize, encrypt/decrypt, sign/validate, print/parse) all benefit from this abstraction. A codec built on profunctors can be adapted with dimap, composed with other codecs, and instantiated with different profunctor types depending on whether you need pure functions, fallible operations, or value extraction.