Aug 18, 2025
Welcome to the first blog post of the Heilmann Software Developer Blog! We are a smallish company in southern Germany, and we create a mobile-first work organisation app for chimney sweeps, using a full Haskell stack🎉 Yes, that's right, Haskell in the backend and the frontend.
In some future blog post, we will talk about our stack in detail. For
now let it suffice to say that we have an extensive model of the work
that a chimney sweep has to do, and we manipulate these nested
structures using good ol' lenses.
You might think that everything important about optics has already been
said, but then I stumbled over something puzzling recently.
We got work to do
In Germany, chimney sweeps do a lot of different work, ranging from actual chimney sweeping to safety inspections. When they have a work package, they have to document meticulously what they have done, or why they weren't able to complete the task. After all, a furnace emitting carbon monoxide can become a death trap, so chimney sweeps are grateful for a smooth application that tracks all work steps reliably.
Let's get to it and model a unit of work!
First, we model three different kinds of work a chimney sweep can do.
-- | One chimney sweep work item
data WorkItem
= SweepChimney (Status SweepMeters)
| CheckFurnace (Status ())
| MeasureCarbonMonoxide (Status CarbonMonoxideMeasurement)
-- | How many meters of chimney or flue were swept.
newtype SweepMeters = SweepMeters Int
deriving newtype (Num)
-- | CO concentration in parts per million.
newtype CarbonMonoxideMeasurement = CarbonMonoxideMeasurement Int
-- Deriving Num allows us to write integer literals as values of this type ;)
deriving newtype (Num, Eq, Ord)
-- | A CO measurement above 1 permille is dangerous and has to be reported!
coLifeThreatening :: CarbonMonoxideMeasurement -> Bool
coLifeThreatening co = co > 1000Note that we have annotated the work items with a
Status, which is parametrised by a type. Let's see what
that is.
A work item can be still to do, it can be done, or it might have been
impossible to do the work because of some unforeseen reason. If it was
done, we want to track how long it took, and we might want to attach
additional information that was specific to this task. For example, when
sweeping a chimney it is relevant how long the chimney is, and when
measuring carbon monoxide, the measured concentration must be tracked.
That's the additional type variable result in the
Status type!
-- | What's the status of the work item?
data Status result
= ToDo
| Done result Minutes
| NotDone Excuse
-- | Why was it not possible to complete the work?
-- Maybe the door to the building was locked and noone opened?
newtype Excuse = Excuse Text
deriving newtype (IsString)
-- | How many minutes did it take to complete the work item. Relevant for billing.
newtype Minutes = Minutes Int
deriving newtype (Num)A focus on optics
We can create optics to work with these types:
makePrisms ''WorkItem
makePrisms ''StatusThis Template Haskell incantation brings a few Prisms
into scope, such as:
_SweepChimney :: Prism' WorkItem (Status SweepMeters)
_CheckFurnace :: Prism' WorkItem (Status ())
_MeasureCarbonMonoxide :: Prism' WorkItem (Status CarbonMonoxideMeasurement)
_ToDo :: Prism' (Status result) ()
_Done :: Prism' (Status result) (result, Minutes)
_NotDone :: Prism' (Status result) ExcuseThe underscore _ in front of every prism name is the
naming convention for prisms in lens.
The tick in Prism' says that this is a "simple" prism.
There is a more general prism type which we won't need today.
These prisms allow us to analyse the different cases of work items and there statuses.
Just your regular optics? Look closer.
If you know your way around van Laarhoven optics or the lens
package just go on reading. If prisms, lenses and optics are new or
unfamiliar for you, we have a little
optics primer prepared for you down below.
All work and no type signature
So let's crunch some data. How about we find out whether there was a really important high CO measurement today, so we can display a big fat red warning sign?
coWarning :: [WorkItem] -> Bool
coWarning = has
$ traversed
. _MeasureCarbonMonoxide
. _Done
. _1
. filtered coLifeThreateningFantastic. Readable, maintainable, succinct, reliable through typechecking. Let's call it a day and cash in the bill!
billedMinutes :: [WorkItem] -> Minutes
billedMinutes = sumOf $ traversed . _allWorkItems . _Done . _2We just need to fill in our hole _allWorkItems, a
traversal that ranges over all work items and plucks out every
Status. From there, we already know how to finish, we focus
on the Done constructor, then on its second field which
contains Minutes, and we have the bill. So let's work this
out.
allWorkItems :: Traversal' Work (Status ...)
allWorkItems = ...Huh. Already the type signature doesn't work out. What should be the
type variable of Status?
First try: Prisms are monoids
A seasoned optics developer might tell you that prisms like
_SweepChimney are monoids, and we can just
<> them together:
allWorkItems = _SweepChimney <> _CheckFurnace <> _MeasureCarbonMonoxideBut GHC will quickly call you out on that:
• Couldn't match type ‘()’ with ‘SweepMeters’
And it's right! After all, the prisms focus on Statuses
applied to different types.
Nevermind, we don't like this solution anyways, since it's too easy
to forget a constructor here! What if we add a constructor to the
WorkItem type definition later and don't add it in the
allWorkItems traversal? Customers would not get billed for
those work items, and that's something they'll quickly complain
about😉
Second try: Hands on
Let's try and write a traversal hands-on. This is more work, but as a bonus GHC would spit out a warning if we forgot a case.
allWorkItems handler (SweepChimney status) = SweepChimney <$> handler status
allWorkItems handler (CheckFurnace status) = CheckFurnace <$> handler status
allWorkItems handler (MeasureCarbonMonoxide status) = MeasureCarbonMonoxide <$> handler statusAgain:
• Couldn't match type ‘()’ with ‘SweepMeters’
No such luck. Probably there is a meaningful reason we can't get this to work.
Third try: A dirty way out
But isn't it weird that we have GHC complaining about types that we don't even care about? Let's look briefly at the type signature:
data Status result
= ToDo
| Done result Minutes
| NotDone ExcuseYes, the whole Status type depends on
result, but Minutes doesn't, so why can't we
just focus on them, and nothing else?
allWorkItemsMinutes :: Traversal' WorkItem Minutes
allWorkItemsMinutes handler (SweepChimney (Done result minutes))
= SweepChimney . Done result <$> handler minutes
allWorkItemsMinutes handler workItem@(SweepChimney _)
= pure workItem
allWorkItemsMinutes handler (CheckFurnace (Done result minutes))
= CheckFurnace . Done result <$> handler minutes
allWorkItemsMinutes handler workItem@(CheckFurnace _)
= pure workItem
allWorkItemsMinutes handler (MeasureCarbonMonoxide (Done result minutes))
= MeasureCarbonMonoxide . Done result <$> handler minutes
allWorkItemsMinutes handler workItem@(MeasureCarbonMonoxide _)
= pure workItemThis works. Tedious and lengthy, but ok. Note that we basically wrote
a traversal over two structures at once: First over
WorkItem, and then over Status. Not very
modular!
Now how about we also create a traversal to focus on all excuses that
were made in a work item? Fingers aren't even twitching: We would need
to redo all of the tedious work again. Same for the ToDo
constructor. And if we extend the Status type, we need to
add new functions for every new constructor. Sounds like bitrot. Even
worse if we reuse Status anywhere else, then we'd have to
redo all of these functions!
Fourth try: We have to focus on something
The issue is that we combined two traversals, one focussing into
WorkItem, and the second one into Status, in
order to hide the result type. Actually, the second of
these steps is not the issue here!
_Done :: Prism' (Status result) (result, Minutes) is a
perfectly valid prism. Yes, it's polymorphic in result, but
that doesn't create a problem.
But in the first step, when diving into a WorkItem, we
want to look at the Status, but ignore everything related
to the result type! We don't care about the exact type of
the status as long as it is, well, some status!
-- If you need this in production use https://hackage.haskell.org/package/some
data Some a = forall result . Some {getSome :: a result}The go-to solution that might spring to mind is an existential type.
Let's wrap the Status type in a Some Status
container, so that users of the container don't know the
result type, but they still know that it's some
type of status. We could go on working with this in principle:
-- | Wrap a traversal with a type variable
withSomeStatus ::
(forall result . Traversal' (Status result) a) ->
Traversal' (Some Status) a
withSomeStatus traversal handler (Some status) = Some <$> traversal handler status
someMinutes :: Traversal' (Some Status) Minutes
someMinutes = withSomeStatus $ _Done . _2All we're missing now is a traversal from WorkItem into
Some Status. Should be easy, right?
workItemSomeStatus :: Traversal' WorkItem (Some Status)
workItemSomeStatus handler (SweepChimney status)
= SweepChimney . (\(Some status) -> status) <$> handler (Some status)
workItemSomeStatus handler (CheckFurnace status)
= CheckFurnace . (\(Some status) -> status) <$> handler (Some status)
workItemSomeStatus handler (MeasureCarbonMonoxide status)
= MeasureCarbonMonoxide . (\(Some status) -> status) <$> handler (Some status)But GHC disagrees:
• Couldn't match type ‘result’ with ‘SweepMeters’
Expected: Status SweepMeters
Actual: Status result
‘result’ is a rigid type variable bound by
a pattern with constructor:
Some :: forall {k} (a :: k -> *) (result :: k). a result -> Some a,
in a lambda abstraction
Somehow, it doesn't agree that the handler returns a
status of the expected type, and insists that the type might be
anything. And... GHC is right! handler has the type
Some Status -> f (Some Status), and nothing prevents us
from changing the result type inside the wrapper! This is
clearly not what we wanted.
Let's zoom out to a broader scope
We had two issues so far where we ran into trouble with the
result type variable. Let's look at them at the same
time.
A quick reminder what a Traversal' is. (The tick still
means that there is a more general notion of traversal which doesn't
matter here.)
type Traversal' s a = forall f . Applicative f =>
(a -> f a) ->
s -> f sOur first and second tries were insert WorkItem for
s and Status result for a:
type FirstAndSecondFail = forall f result . Applicative f =>
(Status result -> f (Status result)) ->
WorkItem -> f WorkItemBut this implied that result must be the same for the
whole traversal, and it can't depend on the focus. Since it changes
based on which kind of WorkItem we are looking at, we get a
type error. Somehow, the scope of result is too broad
The fourth attempt tried to hide the result type very
tightly. Without the data wrapper (and with
ImpredicativeTypes), the type would read as:
type FourthFail = forall f . Applicative f =>
((forall result1 . Status result1) -> f (forall result2 . Status result2)) ->
WorkItem -> f WorkItemI intentionally called the type variables result1 and
result2, because the issue here is precisely that there is
no guarantee that they are the same. Here, the scope of the type
variable is too narrow.
The solution lies in between.
There must be some traversal!
Without further ado, here is the solution. (Well, the type signature of the solution at least. (But you know, in Haskell that's often all that really matters.))
type ThisWorks = forall f .
(forall result . Status result -> f (Status result)) ->
WorkItem -> f WorkItemWe have given the handler an existential type variable. So
once per handler call, result will be some type, and on the
next handler call it can be a different one.
Put differently: When using the handler, you have to make sure that
the result type stays the same.
Let's generalise this again.
type SomeTraversal' s a = forall f . Applicative f =>
(forall x . a x -> f (a x)) ->
s -> f sWe've renamed result to x, generalized
WorkItem to s and Status to
a. Note that a is not a type, it's a type
operator of kind a :: Type -> Type! So let's implement
some traversal.
workItemStatus :: SomeTraversal' WorkItem Status -- Note: No type variable!
workItemStatus handler (SweepChimney status) = SweepChimney <$> handler status
workItemStatus handler (CheckFurnace status) = CheckFurnace <$> handler status
workItemStatus handler (MeasureCarbonMonoxide status)
= MeasureCarbonMonoxide <$> handler statusSweet and simple! But does it blend? I mean, does it blend in with the rest of the code? Does it compose?
minutes :: Traversal' WorkItem Minutes
minutes = workItemStatus . _Done . _2• Couldn't match type: Status result0 -> f (Status result0)
with: forall x. Status x -> f (Status x)
Expected: (Minutes -> f Minutes)
-> forall x. Status x -> f (Status x)
Actual: (Minutes -> f Minutes)
-> Status result0 -> f (Status result0)
Will it never end? What kind of horrid skolem just escaped its scope? But don't worry, there is a relatively simple solution.
infixr 9 .:
-- | Compose 'SomeTraversal'' with a regular traversal
-- in such a way that the type variable vanishes.
(.:) :: SomeTraversal' s a -> (forall x . Traversal' (a x) b) -> Traversal' s b
tsa .: taxb = \bhandler -> tsa (taxb bhandler)The issue is just that the focus of SomeTraversal'
contains an existential type variable, and we need to make sure to
compose it with a traversal that accepts any type in that place. In the
end, if we want to retrieve some data in a concrete type without any
existentials, we have to compose workItemStatus with a
traversal that accepts Status result and then focuses on
something that doesn't mention result, such as
_Done . _2:
minutes :: Traversal' WorkItem Minutes
minutes = workItemStatus .: _Done . _2And that's it! We're back in good ol' standard optics land.
Thanks for reading!
Addenda
What, you're still reading? Ok, there is a bit more to say. A bit advanced material in this section, you have been warned!
Some action
Inevitably, you might want to retrieve the status of a work item. In
standard lens, you can do this with an action such
as ^?:
(^?) :: Traversal' s a -> s -> Maybe aOf course, this won't work here:
sweepingChimneyStatus :: Maybe (Status SweepMeters)
sweepingChimneyStatus = SweepChimney (Done 23 42) ^? workItemStatus• Couldn't match type: forall x.
Status x
-> Const (Data.Monoid.First (Status SweepMeters)) (Status x)
with: Status SweepMeters
-> Const
(Data.Monoid.First (Status SweepMeters)) (Status SweepMeters)
Never mind the Const (Data.MonoidFirst ...) bit. That's
got to do with the kind of operation we are performing. The important
bit is that forall x . Status x ... is not
Status SweepMeters. We're not allowed to sneak in a
forall x here.
And it kind of makes sense! Our new existential traversal makes a big point of not knowing the type parameter, so we shouldn't be expecting an action on it to be able to specify that type.
Instead, we need to hide it somehow. Remember
Some Status?
data Some a = forall result . Some {getSome :: a result}We can use this to hide the result type of the output of
our action:
(^?:) :: SomeTraversal' s a -> s -> Maybe (Some a)
stsa ^?: s = getFirst $ getConst $ stsa (Const . First . Just . Some) sOther optics
Everything we've done here applies to other kinds of optics as well, as long as they are encoded in van-Laarhoven style. A very general optic can be defined like this:
type SomeLensLike f s t a b = (forall tag. a tag -> f (b tag)) -> s -> f tIf you quantify f and give it type classes like
Functor, Applicative, ..., you recover
existential lenses, traversals, and so on.
Other encodings
When people hear about optics and existentials, they often think about the existential encoding of optics. This is completely orthogonal to what is discussed here, but I believe the two can be combined! Homework exercise for you.
Profunctors
Nowadays, no blog post about optics is complete without a mention of profunctor optics. So here it is!
This whole spiel can also be generalised to profunctor optics:
-- | Hide a type variable in the input & output types of the profunctor
data SomeProfunctor p a b = forall x . SomeProfunctor {getSomeProfunctor :: p (a x) (b x)}
-- This goes from profunctors on regular Haskell types to type operators!
type SomeProfunctorOptic' p s a = p s s -> SomeProfunctor p a aFor p = (->), we recover our existential optics from
before. But with this generalisation, we can also tackle prisms, indexed
optics, and so on.
A quick optics primer
Prisms and lenses are special cases of "optics". Optics are a particular family of constructions to analyse manipulate complex nested data structures. There are many different concepts and implementations of optics with numerable tradeoffs which we won't cover here. We are concerned with a very direct and simple approach to optics, the "van Laarhoven style", where optics are simply a particular sort of higher order functions.
Let's look at one particular kind of optics that generalises prisms and lenses, namely traversals.
Often we have a nested data structure that has some small bits deep
inside it, and we want to, well, "do" something with them. For example,
extract, overwrite, or modify them. In our chimney sweep example, we
might want to go through a list of work items and filter out all the
excuses. Or sum up all of the Minutes of work. Or check
whether there was a dangerous carbon monoxide measurement anywhere in
the list. And so on.
A very general pattern capturing many kinds of operations on a data structure is a traversal:
type Traversal' s a
= forall f . (Applicative f)
=> (a -> f a)
-> s -> f sThis is a quite a bit to unwrap here. The s type is the
big, nested data structure. a is the type of "small bits",
which we'll call the focus in the following. And f
is the kind of operation we are allowed to do, we'll see in a second how
that can be.
The big picture here is that a traversal accepts a handler
a -> f a which operates on a focus, and returns a
function s -> f s. In our example, a might
be the type Minutes, while s might be
[WorkItem] (or even some bigger data structure like
Map Date [WorkItem]).
With the handler, a user of the traversal can specify what should happen with each focus. For example, we might want to modify the minutes value and multiply it by 2 in order to pretend that all the work took twice as long today😉 Or we could sum up all the minutes. The traversal then aggregates all the operations on the foci in one operation on the big structure.
This is where the forall f part comes in. The user can
not only choose the handler, it can also choose the type of
f, that is, they can choose what kind of operation to
perform! For example, f = Identity means that the handler
is isomorphic to Minutes -> Minutes, and the resulting
function of type [WorkItem] -> [WorkItem], so we can
modify each focus with the handler, and get a modified data structure. A
sort of nested functor! But we could also choose
f = Const (Sum Minutes), so the handler type becomes
Minutes -> Const (Sum Minutes) which is just a tedious
wrapping of newtypes, and the resulting function is
[WorkItem] -> Const (Sum Minutes). Magically, we can sum
up all minutes of work with the same traversal. The only restriction
about f is Applicative f, but there are many
useful such fs around.
You may say that these traversals sound nice and powerful, but how
would we create them? Luckily, all Prism's from above
are special cases of traversals!
Also, every Traversable structure (lists, maps, even
your own types when you DeriveTraversable!) gives a
traversal:
traversed :: Traversable t => Traversal' (t a) aAnd what's more, the lens package also provides
Lens'es which focus on parts of a tuple:
_1 :: Lens' (result, Minutes) result
_2 :: Lens' (result, Minutes) MinutesSince every lens is also a traversal, we now have a huge library of these structures to play with.
Are you ready for the punch line?
🥁🥁🥁
Optics compose.
Given a traversal from WorkItem to
Status SweepMeters, such as _SweepChimney, and
a traversal from Status SweepMeters to Excuse,
such as _NotDone, we can create a composed traversal:
chimneyNotSwept :: Traversal' WorkItem Excuse
chimneyNotSwept = _SweepChimney . _NotDoneFinally, a note on what is often called "actions" on an optic. We
already said that choosing the applicative functor f
basically determines what kind of operation we perform on the data
structure. But often the result type is a bit unwieldy because of many
newtype wrappers (like Const, Identity,
Sum, and so on), so it is useful to create little helper
functions that monomorphize f to a particular type, and
unwraps all the newtypes.
For example, let's choose f x = Const [a] x, which is an
applicative because [a] is a monoid:
traversalToList :: s -> Traversal' s a -> [a]
traversalToList s t = s & t (Const . pure) & getConstOther popular examples (with simplified type signatures) from the lens library include:
-- | Whether the traversal has any matches
has :: Traversal' s a -> s -> Bool
-- | Compute the sum of all foci
sumOf :: Num a => Traversal' s a -> s -> aThat's it for now! If you've read and understood this crash course, and your brain hasn't exploded yet, congratulations! You can jump back to the main post.
