10

The following simple function applies a given monadic function iteratively until it hits a Nothing, at which point it returns the last non-Nothing value. It does what I need, and I understand how it works.

lastJustM :: (Monad m) => (a -> m (Maybe a)) -> a -> m a
lastJustM g x = g x >>= maybe (return x) (lastJustM g)

As part of my self-education in Haskell I'm trying to avoid explicit recursion (or at least understand how to) whenever I can. It seems like there should be a simple non-explicitly recursive solution in this case, but I'm having trouble figuring it out.

I don't want something like a monadic version of takeWhile, since it could be expensive to collect all the pre-Nothing values, and I don't care about them anyway.

I checked Hoogle for the signature and nothing shows up. The m (Maybe a) bit makes me think a monad transformer might be useful here, but I don't really have the intuitions I'd need to come up with the details (yet).

It's probably either embarrassingly easy to do this or embarrassingly easy to see why it can't or shouldn't be done, but this wouldn't be the first time I've used self-embarrassment as a pedagogical strategy.

Update: I could of course provide a predicate instead of using Maybe: something like (a -> Bool) -> (a -> m a) -> a (returning the last value for which the predicate is true) would work just as well. What I'm interested in is a way to write either version without explicit recursion, using standard combinators.


Background: Here's a simplified working example for context: suppose we're interested in random walks in the unit square, but we only care about points of exit. We have the following step function:

randomStep :: (Floating a, Ord a, Random a) =>
              a -> (a, a) -> State StdGen (Maybe (a, a))
randomStep s (x, y) = do
  (a, gen') <- randomR (0, 2 * pi) <$> get
  put gen'
  let (x', y') = (x + s * cos a, y + s * sin a)
  if x' < 0 || x' > 1 || y' < 0 || y' > 1
    then return Nothing
    else return $ Just (x', y')

Something like evalState (lastJustM (randomStep 0.01) (0.5, 0.5)) <$> newStdGen will give us a new data point.

Community
  • 1
  • 1
Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • 2
    `lastJustM = fix (liftM2 ap ((>>=) .) . (flip (maybe . return) .))`. (OK I cheated with `pointfree`.) – kennytm May 18 '10 at 18:30
  • 1
    @KennyTM: Thanks! I didn't even think to try `pointfree` because I had no idea it would be able to handle this kind of thing. Now I just have to figure out how it works. – Travis Brown May 18 '10 at 18:43
  • 1
    There's an algorithm to reduce *anything* to pointfree form given a handful of combinators; this is what `pointfree` uses. Of course, the result may or may not be useful :) – Antal Spector-Zabusky May 18 '10 at 19:25
  • @absz: Yes, now I see that `fix` is kind of a cheap trick here. I still think it's cool that the result is as concise as it is in this case, even though I wouldn't use it in real code. – Travis Brown May 18 '10 at 19:43
  • 1
    Don't be confused by the name, the proper term for the output of `pointfree` is "pointless style". And is that `liftM2` working in the `((->) r)` monad? That always improves the clarity of code... – C. A. McCann May 19 '10 at 00:38
  • @camccann: I don't know about "pointless"—I think I've learned a lot by thinking through the output of `pointfree`, even when I don't actually use the code that it produces. – Travis Brown May 19 '10 at 02:02
  • @KennyTM `fix` is just another word for explicit recursion. – sigfpe May 19 '10 at 03:49
  • 2
    The real problem with `fix` is that it has no semantic value other than "crash the program if I messed up". The value of things like `map` and `fold` is that, as long as you don't left fold an infinite list, they perfectly define the operation they perform and won't lead you astray. – C. A. McCann May 19 '10 at 04:07

2 Answers2

9

A lot of what avoiding explicit recursion is about is composing built-in recursive combinators, which usually work on very generic unlifted values. Doing the same thing in a Functor, Monad, or other lifted type sometimes works using basic lifting operations like fmap, <*>, >>=, and so on. In some cases a pre-lifted version is already present, as with mapM, zipWithM, and so on. Other times, as with takeWhile, lifting is not trivial and no built-in version is provided.

Your function indeed has characteristics of something that should be a lifted version of standard combinators. So first, let's strip out the monad to reconstruct the function you're implicitly lifting:

lastJust :: (a -> Maybe a) -> a -> a

The word "last" here gives us a hint; non-explicit recursion often uses temporary lists as control structures. So what you want is last applied to the list generated by iterating the function until getting Nothing. With a slight generalization of the type, we find the generator:

unfoldr :: (b -> Maybe (a, b)) -> b -> [a]

So we have something like this:

dup x = (x, x)
lastJust f x = last $ unfoldr (fmap dup . f) x

Unfortunately at this point we're kind of stuck, because (to my knowledge) there's no monadic unfold and lifting it is, like takeWhile, not trivial. Another thing that could make sense is a more general unfold with a signature like (MonadMaybe m) => (b -> m (a, b)) -> b -> m [a] and an accompanying MaybeT transformer, but that doesn't exist in the standard libraries either, and monad transformers are kind of a Pit of Despair anyway. A third approach might be to find some way to generalize both Maybe and the unknown monad out as MonadPlus or something similar.

Of course, there may be other approaches with a different structure, but I suspect you're likely to find similar awkwardness with any function that needs to be recursive going "into" a monad, e.g., each step conceptually introduces another layer that must be eliminated with >>=, join, etc.

In summary: At first inspection your function as written is best expressed without explicit recursion, using a recursive combinator (some flavor of unfoldM) that doesn't exist. You can either write the combinator yourself (as people have done with takeWhileM), go digging around on Hackage for monadic recursive combinators, or just leave your code as is.

C. A. McCann
  • 76,893
  • 19
  • 209
  • 302
  • Nice answer, but I find the recursive version frightfully perspicuous. Do you really think it would be improved by using some sort of monadic unfolding combinator instead? – Norman Ramsey May 19 '10 at 03:28
  • 1
    @Norman Ramsey: Maybe (no pun intended), I suppose. I like standard combinators when they make semantic structure more obvious, in this case that it implicitly generates and collapses a list of results (a hylomorphism, if I remember the terminology correctly?). For the same reason, I like to code in monads, etc. by lifting the underlying logic e.g., `<$>` and `<*>` instead of complex, imperative-looking `do` blocks. In this case the function is straightforward enough as is and the necessary combinator doesn't exist, so I likely wouldn't bother, myself. – C. A. McCann May 19 '10 at 04:02
  • @Norman Ramsey: On the other hand, it may just be a sign that I need to lay off the Category Theory textbooks and try to stop noticing corecursion everywhere... – C. A. McCann May 19 '10 at 04:04
  • @Norman Ramsey: In camccann's defense I wasn't really asking for an improvement or for something that would replace the recursive version in my code—it was more a mental exercise, and I found this answer very helpful in that respect. – Travis Brown May 19 '10 at 04:06
  • @camccann: as for the combinators from Hackage that you suggest looking for, check my answer below which gives pointers to such modular combinators. – yairchu May 19 '10 at 12:12
  • @Travis, Oh, as a mental exercise it's terribly impressive. I was with camccann right up to the point where he said "best expressed"---then I boggled. I would have said, "it's probably best expressed *with* explicit recursion, but if you want to go without, the best plan is to ...". But then, category theory makes my head hurt. @Camccann, what textbooks do you like? – Norman Ramsey May 19 '10 at 14:39
  • @Norman Ramsey: The pain is how you know it's working! "Textbook" might be too strong a word, rather I've accumulated various books/articles in electronic form. Fairly easy stuff--currently working through "an introduction to Category Theory in four easy movements" and Awodey's "Category Theory", mostly with an eye to relating concepts back to programming. As for Travis's function, "best expressed" was probably overstating a matter of personal taste. If I couldn't make it look very like my `lastJust` I'd leave the explicit recursion. Though, I do like yairchu's version as well. – C. A. McCann May 19 '10 at 16:35
3

I don't want something like a monadic version of takeWhile, since it could be expensive to collect all the pre-Nothing values, and I don't care about them anyway.

Monadic-lists takeWhile does not collect all the pre-Nothing values unless you explicitly want to do that. This would be the takeWhile from the "List" package, used in this answer to the very question you linked to.

As for the function that you wish to implement:

{-# LANGUAGE ScopedTypeVariables #-}

import Control.Monad.ListT (ListT) -- from "List" package on hackage
import Data.List.Class (takeWhile, iterateM, lastL)
import Prelude hiding (takeWhile)

thingM :: forall a m. Monad m => (a -> Bool) -> (a -> m a) -> m a -> m a
thingM pred stepM startM =
    lastL $ takeWhile pred list
    where
        list :: ListT m a
        list = iterateM stepM startM
Community
  • 1
  • 1
yairchu
  • 23,680
  • 7
  • 69
  • 109
  • I'm interested in learning more about monadic lists—I still don't understand exactly how they avoid collecting the values, for example. I haven't had a chance to work through the "ListT done right" page on the wiki yet, but do you have any suggestions for other starting points that I wouldn't be likely to find by Googling "listt haskell", "monadic lists", etc.? – Travis Brown May 19 '10 at 13:00
  • 1
    @Travis Brown: I'm not sure what you mean by "collecting the values" here. If list elements are being discarded as fast as they're created, the full list will never actually be built all at once. This is what I meant above about lists being a "control structure"--folding a list is basically recursion with the call stack specified "ahead of time", often lazily. – C. A. McCann May 19 '10 at 13:22
  • @camccann: I think I'm just confusing myself about the way the monad and laziness interact here. I'll shut up about it until I have a chance to do some more reading and thinking. Thanks for your answers. – Travis Brown May 19 '10 at 13:36
  • @Travis Brown: Laziness is lazy, until it isn't, unfortunately. Monads in general don't matter much, but the machinery hidden inside the `>>=` operation can add varying degrees of strictness. `Identity` forces the chain of binds, `State` forces extra strictness on use of the state parameter, `IO` forces full strictness on things that touch the outside world, &c. Not forcing more than necessary is what all these monadic list combinators are for; otherwise the naive `takeWhileM f xs = sequence xs >>= (return . takeWhile f)` would work. – C. A. McCann May 19 '10 at 14:09
  • 1
    @Travis Brown: with normal lists, `takeWhile` doesn't necessarily mean that the takeWhiled part of the list is stored in memory - it all depends on what is done with the resulting list afterwards. same case here. the monadic `takeWhile` does not consume the list "when you call it", it returns another list. Depending on what you do with it later, it may be stored in memory, or not. in this case it is not stored in memory because `lastL` consumes it without saving its values. – yairchu May 19 '10 at 21:06