3
\$\begingroup\$

I am working my way through the Haskell Book and ran into the exercises at the end of the Monad chapter(18). One in particular asked to define a function with the following signature:

meh :: Monad m => [a] -> (a -> m b) -> m [b]

So I did. I found this routine difficult to implement at first, but once I understood what I was doing and what I needed to do a little better I figured it out.

I have three versions of the routine, my first attempt, a slightly refactored attempt, and a final attempt using do-notation. I was wondering if anyone can give advice on how I could potentially refactor this routine further, or how I can make my implementation better. I feel like I can make the routine far cleaner than I have it, but I am not sure how.

WARNING: If you are reading the Haskell Book and want to work through the exercises yourself, I recommend you do not read any further

Here are all three implementations

-- Attempt #1 with helper routine
meh :: Monad m => [a] -> (a -> m b) -> m [b]
meh list fn = doWork list (return $ []) fn
  where 
    doWork (x:xs) base f
      | length xs == 0 = (addToMonad x base f) >>= (\x -> return $ reverse x)
      | otherwise = doWork xs (addToMonad x base f) f

addToMonad x baseMonad fn = do
  someVal <- (fn x)
  base <- baseMonad
  return $ someVal:base

-- Attempt #2 with a fold
meh' :: Monad m => [a] -> (a -> m b) -> m [b]
meh' list fn = (foldl (\acc x -> (fn x) >>= (\y -> acc >>= (\z -> return $ y:z))) (return $ []) list) >>= (\x -> return $ reverse x)

-- Attempt #3 with a fold and do-notation
mehDo :: Monad m => [a] -> (a -> m b) -> m [b]
mehDo list fn = do 
  final <- (foldl (\acc x -> do
    val <- (fn x) 
    mList <- acc
    return $ val:mList)
    (return $ []) list)
  return $ reverse final

I want to say my last implementation is the easiest to read (it would be easier with better names, IMO) but I don't really know that it is. I am not super experienced in the ways of Haskell, so to be frank I read the routines really slowly regardless.

Anyway, advice or tips on how to make this cleaner would be much appreciated.

\$\endgroup\$
1
\$\begingroup\$

I'd say that the idiomatic way to write this function in Haskell is by composing a more primitive one with a map. Indeed, given the type signature of meh, it is very tempting to "prepare" a list of all the m b actions and then combine them in order:

meh :: Monad m => [a] -> (a -> m b) -> m [b]
meh xs f = combine (f <$> xs)

Now, combine has type [m b] -> m [b]. To process that list, we can use a fold. You have used foldl in your examples but you ended up reversing the list which is a clear sign that you may have wanted a foldr instead. And we can indeed do that:

combine :: Monad m => [m b] -> m [b]
combine = foldr (\ x xs -> (:) <$> x <*> xs) (return [])

Here the functor (<$>) and applicative (<*>) combinators allow to write something that looks a bit direct style (we are basically using (:) to construct a list by giving its head and tail) but that takes care of the monadic actions.

\$\endgroup\$
1
\$\begingroup\$

As I don't have a copy of the Haskell Book, I don't know what is already known at that point. But meh is a standard function, namely flip mapM or forM:

import Control.Monad (forM)

meh :: Monad m => [a] -> (a -> m b) -> m [b]
meh = forM

But that's cheating, right? So we can try another approach instead, namely split the functionality into two functions:

meh :: Monad m => [a] -> (a -> m b) -> m [b]
meh xs f = sequence' (map f xs)

sequence is again a standard function, so we're still somewhat cheating. However, we can write it our own:

sequence' :: Applicative f => [f a] -> f [a]
sequence' []     = pure []
sequence' (x:xs) = (:) <$> x <*> sequence' xs

Note that Applicative is enough to define both meh and sequence'. The original sequence is usually defined as follow, by the way:

sequence []     = return []
sequence (x:xs) = do
    y  <- x
    ys <- sequence xs
    return (y : ys)

Which isn't too far off of your last variant. But if you're already using do notation, I would pair it with pattern-matching:

mehDo :: Monad m => [a] -> (a -> m b) -> m [b]
mehDo []     f = return []
mehDo (x:xs) f = do
   y  <- f x
   ys <- mehDo xs f
   return (y : ys)

Which, in my point of view, is the easiest variant to read. But if you use meh in your own code, you're going to use forM either way.

\$\endgroup\$
  • \$\begingroup\$ This was very informative. It is neat to see things like Foldable and Traversable that can be exchanged with lists as you get deeper into Haskell. Thanks for your time. \$\endgroup\$ – Carson Mar 14 '17 at 14:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.