At Runtime Verification, we are using Haskell to develop the next generation of formal verification tools based on the K Framework. This article describes how we use algebraic data types to write expressive Haskell code.
Bool represents a single bit of information:
data Bool = False | True
The popular term “boolean blindness” refers to the information lost by functions that operate on
Bool when richer structures are available. Erasing such structure can give code a bad smell. Using more structure can produce interfaces that are easier to document, use, decompose, and generalize.
Data.List.filter is a classic example of a function that would benefit from a richer interface type:
filter :: (a -> Bool) -> [a] -> [a] -- ^^^^^^^^^ -- predicate: keep or discard each element
A “filter” has two uses:
- To collect the fluid (filtrate) and discard the particulate, such as when making coffee.
- To collect the particulate and discard the filtrate, such as when collecting gold.
Just as there are two uses for a filter, I can never remember if
filter’s predicate means “keep” or “discard”. For the record, the
Prelude definition is:
filter _  =  filter keep (a : as) | keep a = a : filter keep as | otherwise = filter keep as
… I think. If I had the other use of “filter” in mind when I wrote the function, I might have written instead,
filter' :: (a -> Bool) -> [a] -> [a] filter' _  =  filter' discard (a : as) | discard a = filter' discard as | otherwise = a : filter' discard as
filter' are both perfectly fine functions, only their intent differs slightly.
One (ahem) solution to clarify intent is to give
filter a better name, one that does not have a dual identity:
select :: (a -> Bool) -> [a] -> [a] -- ^^^^^^^^^ -- predicate: select each element select _  =  select keep (a : as) | keep a = a : select keep as | otherwise = select keep as
An alternative is to supply rename the type and constructors expressively,
data Keep = Discard | Keep filter1 :: (a -> Keep) -> [a] -> [a] filter1 _  =  filter1 keep (a : as) | Keep <- keep a = a : filter1 keep as | otherwise = filter1 keep as
This popular style has a practical disadvantage: there are already many functions that work with
Bool, but none will work with
Keep unless we reimplement them ourselves.
Including more structure in the type of
filter can make the definition more general while expressing our intent more clearly in the code. The most important feature of
filter is that each element of the input list corresponds to zero (
Discard) or one (
Keep) elements of the output. There is already a type which structurally represents zero or one elements,
data Maybe a = Nothing | Just a
We can use
Maybe to express the intent that each input element yields zero or one output elements:
filter2 :: (a -> Maybe a) -> [a] -> [a] filter2 _  =  filter2 keep (a : as) | Just b <- keep a = b : filter2 keep as | otherwise = filter2 keep as
filter2 is a generalization of
filter because we might transform the list as we filter it:
-- | Select only the positive elements and decrement them. decrementPositive :: [Integer] -> [Integer] decrementPositive = filter2 (\x -> if x > 0 then Just (x - 1) else Nothing)
Although the type of
filter2 is quite suggestive to the programmer, it is still not fully faithful to our intent! Consider the following implementation with the same type, which has a subtle bug:
filter2' :: (a -> Maybe a) -> [a] -> [a] filter2' _  =  filter2' keep (a : as) | Just b <- keep a = a : filter2' keep as | otherwise = filter2' keep as
We can use parametric polymorphism to express our intent faithfully in the code,
filter3 :: (a -> Maybe b) -> [a] -> [b] filter3 _  =  filter3 keep (a : as) | Just b <- keep a = b : filter3 keep as | otherwise = filter3 keep as
The type of
filter3 ensures that its implementation only collects outputs from the predicate, because that is the only thing in scope which can produce a value of
b. Note that we did not have to change the implementation because it was already correct! We only needed to change the type to express our intent more clearly. Changing the type made the implementation still more general: the predicate may now even transform the elements to a different type while filtering the list.
Generalization, composition, and decomposition
filter3 is not an evocative name, and in generalization we have strayed from the original use of “filter”. Luckily, we do not need to name the function ourselves: it already exists as
Data.Witherable.mapMaybe. The original
filter is implemented with a little help:
keeping :: (a -> Bool) -> (a -> Maybe a) keeping predicate a | predicate a = Just a | otherwise = Nothing filter'' :: (a -> Bool) -> [a] -> [a] filter'' predicate = mapMaybe (keeping predicate)
filter encourages to build a reusable components like
keeping also allows us to reuse all the convenient definitions
Prelude provides for
Post script: How far is too far?
It might be tempting to carry on generalizing. Instead of allowing zero or one output elements per input, we might allow any number of outputs:
filter4 :: (a -> [b]) -> [a] -> [b] filter4 _  =  filter4 keep (a : as) = keep a ++ filter4 keep as
This function is probably too general to be useful as a generalization of
filter: by allowing any number of outputs, we allow so many transformations that it makes the intent unclear. This generalization does appear, though, in the
Monad instance of lists:
flip (>>=) :: (a -> [b]) -> [a] -> [b] -- flip (>>=) === filter4
In the context of
filter, we may have gone too far, but in another context this might be exactly the abstraction we need.