Monadic List Functions

Posted on February 11, 2018

Here’s an old Haskell chestnut:

>>> filterM (\_ -> [ False , True ]) [ 1 , 2 , 3 ] filterM (\_]) [ 3 ],[ 2 ],[ 2 , 3 ],[ 1 ],[ 1 , 3 ],[ 1 , 2 ],[ 1 , 2 , 3 ]] [[],[],[],[],[],[],[],[]]

filterM (\_ -> [False,True]) gives the power set of some input list. It’s one of the especially magical demonstrations of monads. From a high-level perspective, it makes sense: for each element in the list, we want it to be present in one output, and not present in another. It’s hard to see how it actually works, though. The (old ) source for filterM doesn’t help hugely, either:

filterM :: ( Monad m) => (a -> m Bool ) -> [a] -> m [a] m)(a[a]m [a] = return [] filterM _ [][] : xs) = do filterM p (xxs) <- p x flgp x <- filterM p xs ysfilterM p xs return ( if flg then x : ys else ys) flgysys)

Again, elegant and beautiful (aside from the three-space indent), but opaque. Despite not really getting how it works, I was encouraged by its simplicity to try my hand at some of the other functions from Data.List.

Grouping

Let’s start with the subject of my last post. Here’s the implementation:

groupBy :: (a -> a -> Bool ) -> [a] -> [[a]] (a[a][[a]] = build (\c n -> groupBy p xsbuild (\c n let f x a q f x a q | q x = (x : ys, zs) q x(xys, zs) | otherwise = ([], c (x : ys) zs) ([], c (xys) zs) where (ys,zs) = a (p x) (ys,zs)a (p x) in snd ( foldr f ( const ([], n)) xs ( const False ))) f (([], n)) xs ()))

It translates over pretty readily:

groupByM :: Applicative m => (a -> a -> m Bool ) -> [a] -> m [[a]] (a[a]m [[a]] = groupByM p xs fmap snd ( foldr f ( const ( pure ([], []))) xs ( const ( pure ( False )))) f (([], []))) xs ()))) where = liftA2 st (q x) (a (p x)) where f x a qliftA2 st (q x) (a (p x)) st b (ys,zs) | b = (x : ys, zs) (xys, zs) | otherwise = ([], (x : ys) : zs) ([], (xys)zs)

Let’s try it with a similar example to filterM :

>>> groupByM (\_ _ -> [ False , True ]) [ 1 , 2 , 3 ] groupByM (\_ _]) [ 1 ],[ 2 ],[ 3 ]],[[ 1 ],[ 2 , 3 ]],[[ 1 , 2 ],[ 3 ]],[[ 1 , 2 , 3 ]]] [[[],[],[]],[[],[]],[[],[]],[[]]]

It gives the partitions of the list!

Sorting

So these monadic generalisations have been discovered before, several times over. There’s even a package with monadic versions of the functions in Data.List. Exploring this idea with a little more formality is the paper “All Sorts of Permutations” (Christiansen, Danilenko, and Dylus 2016), and accompanying presentation on YouTube. They show that the monadic version of sort produces permutations of the input list, and examine the output from different sorting algorithms. Here’s a couple of their implementations, altered slightly:

insertM :: Monad m => (a -> a -> m Bool ) -> a -> [a] -> m [a] (a[a]m [a] = pure [x] insertM _ x [][x] @ (y : ys) = do insertM p x yys(yys) <- p x y ltep x y if lte lte then pure (x : yys) (xyys) else fmap (y : ) (insertM p x ys) (y) (insertM p x ys) insertSortM :: Monad m => (a -> a -> m Bool ) -> [a] -> m [a] (a[a]m [a] = foldrM (insertM p) [] insertSortM pfoldrM (insertM p) [] partitionM :: Applicative m => (a -> m Bool ) -> [a] -> m ([a],[a]) (a[a]m ([a],[a]) = foldr f ( pure ([],[])) where partitionM pf (([],[])) = liftA2 ifStmt (p x) where f xliftA2 ifStmt (p x) ifStmt flg (tr,fl) | flg = (x : tr,fl) flg(xtr,fl) | otherwise = (tr,x : fl) (tr,xfl) quickSortM :: Monad m => (a -> a -> m Bool ) -> [a] -> m [a] (a[a]m [a] = pure [] quickSortM p [][] : xs) = do quickSortM p (xxs) <- partitionM (p x) xs (gt,le)partitionM (p x) xs <- quickSortM p le lsquickSortM p le <- quickSortM p gt gsquickSortM p gt pure (ls ++ [x] ++ gs) (ls[x]gs) >>> insertSortM (\_ _ -> [ False , True ]) [ 1 , 2 , 3 ] insertSortM (\_ _]) [ 1 , 2 , 3 ],[ 1 , 3 , 2 ],[ 3 , 1 , 2 ],[ 2 , 1 , 3 ],[ 2 , 3 , 1 ],[ 3 , 2 , 1 ]] [[],[],[],[],[],[]] >>> quickSortM (\_ _ -> [ False , True ]) [ 1 , 2 , 3 ] quickSortM (\_ _]) [ 3 , 2 , 1 ],[ 2 , 3 , 1 ],[ 2 , 1 , 3 ],[ 3 , 1 , 2 ],[ 1 , 3 , 2 ],[ 1 , 2 , 3 ]] [[],[],[],[],[],[]]

As it should be easy to see, they’re very concise and elegant, and strongly resemble the pure versions of the algorithms.

State

So the examples above are very interesting and cool, but they don’t necessarily have a place in real Haskell code. If you wanted to find the permutations, partitions, or power set of a list you’d probably use a more standard implementation. That’s not to say that these monadic functions have no uses, though: especially when coupled with State they yield readable and fast implementations for certain tricky functions. ordNub , for instance:

ordNub :: Ord a => [a] -> [a] [a][a] = ordNub flip evalState Set.empty . evalState Set.empty filterM -> do (\x <- gets (Set.notMember x) flggets (Set.notMember x) when flg (modify (Set.insert x)) pure flg) flg)

Alternatively, using a monadic version of maximumOn :

maximumOnM :: ( Applicative m, Ord b) => (a -> m b) -> [a] -> m ( Maybe a) m,b)(am b)[a]m (a) = ( fmap . fmap ) snd . foldl f ( pure Nothing ) maximumOnM pf ( where = liftA2 g a (p e) f a eliftA2 g a (p e) where g Nothing q = Just (q, e) (q, e) @ ( Just (o, y)) q g b(o, y)) q | o < q = Just (q, e) (q, e) | otherwise = b

You can write a one-pass mostFrequent :

mostFrequent :: Ord a => [a] -> Maybe a [a] = mostFrequent flip evalState Map.empty . evalState Map.empty maximumOnM -> maybe 1 succ <$> state (Map.insertLookupWithKey ( const ( + )) x 1 )) (\xstate (Map.insertLookupWithKey ()) x))

Decision Trees

One of the nicest things about the paper was the diagrams of decision trees provided for each sorting algorithm. I couldn’t find a library to do that for me, so I had a go at producing my own. First, we’ll need a data type to represent the tree itself:

data DecTree t a t a = Pure a | Choice t ( DecTree t a) ( DecTree t a) t (t a) (t a) deriving Functor

We’ll say the left branch is “true” and the right “false”. Applicative and monad instances are relatively mechanical :

instance Applicative ( DecTree t) where t) pure = Pure Pure f <*> xs = fmap f xs xsf xs Choice c ls rs <*> xs = Choice c (ls <*> xs) (rs <*> xs) c ls rsxsc (lsxs) (rsxs) instance Monad ( DecTree t) where t) Pure x >>= f = f x f x Choice c ls rs >>= f = Choice c (ls >>= f) (rs >>= f) c ls rsc (lsf) (rsf)

We can now create a comparator function that constructs one of these trees, and remembers the values it was given:

traceCompare :: a -> a -> DecTree (a,a) Bool (a,a) = Choice (x,y) ( Pure True ) ( Pure False ) traceCompare x y(x,y) () (

Finally, to draw the tree, I’ll use a function from my binary tree library:

printDecTree :: ( Show a, Show b) => String -> DecTree (a,a) b -> IO () a,b)(a,a) b() = putStr (drawTreeWith id (go t) "" ) where printDecTree rel t(drawTreeWith(go t) Pure xs) = Node ( show xs) Leaf Leaf go (xs)xs) Choice (x,y) tr fl) = go ((x,y) tr fl) Node ( show x ++ rel ++ show y) (go tr) (go fl) rely) (go tr) (go fl)

And we get these really nice diagrams out:

>>> (printDecTree "<=" . insertSortM traceCompare) [ 1 , 2 , 3 ] (printDecTreeinsertSortM traceCompare) [ 1 , 2 , 3 ] ┌[ ┌ 1 <= 2┤ 2┤ 2 , 1 , 3 ] │ │ ┌[ 1 <= 3┤ │ └3┤ 2 , 3 , 1 ] │ └[ 2 <= 3┤ 3┤ 1 , 3 , 2 ] │ ┌[ └ 1 <= 3┤ 3┤ 3 , 1 , 2 ] │ ┌[ └ 1 <= 2┤ 2┤ 3 , 2 , 1 ] └[ >>> (printDecTree "<=" . quickSortM traceCompare) [ 1 , 2 , 3 ] (printDecTreequickSortM traceCompare) [ 1 , 2 , 3 ] ┌[ ┌ 2 <= 3┤ 3┤ 1 , 3 , 2 ] │ └[ ┌ 1 <= 3┤ 3┤ 3 , 1 , 2 ] │ └[ 1 <= 2┤ 2┤ 2 , 1 , 3 ] │ ┌[ └ 1 <= 3┤ 3┤ 2 , 3 , 1 ] │ ┌[ └ 2 <= 3┤ 3┤ 3 , 2 , 1 ] └[

We can also try it out with the other monadic list functions:

>>> (printDecTree "=" . groupByM traceCompare) [ 1 , 2 , 3 ] (printDecTreegroupByM traceCompare) [ 1 , 2 , 3 ]] ┌[[]] ┌ 2 = 3┤ 3┤ 1 , 2 ],[ 3 ]] │ └[[],[]] 1 = 2┤ 2┤ 1 ],[ 2 , 3 ]] │ ┌[[],[]] └ 2 = 3┤ 3┤ 1 ],[ 2 ],[ 3 ]] └[[],[],[]]

Applicative

You might notice that none of these “monadic” functions actually require a monad constraint: they’re all applicative. There’s a straightforward implementation that relies only on applicative for most of these functions, with a notable exception: sort. Getting that to work with just applicative is the subject of a future post.

References