Avoiding explicit recursion in Haskell
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))
rand开发者_如何学ComStep 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.
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.
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
精彩评论