问题
That is, what I am asking about is a loop.
effectful :: Int -> IO Int
effectful n = do
putStrLn $ "Effect: " ++ show n
return n
condition = (== 3)
final :: Int -> IO ()
final n = putStrLn $ "Result: " ++ show n
loop = ?
It should work like this:
λ loop [1..10]
Effect: 1
Effect: 2
Effect: 3
Result: 3
I can offer a recursive definition:
loop (x: xs) = do
r <- effectful x
if condition r
then final r
else loop xs
However, I am having trouble representing this effect with any combination of Functor
, Monad
,
Foldable
and Traversable
methods, because they always insist on evaluating all of the actions,
while what I need is to stop at some point within the list.
For instance, with an unfoldrM
(which is an effectful version of Data.List.unfoldr
that I
made up for the occasion) I can perform exactly the actions I need, but I cannot attain the value
of the last action, because the function parameter returns Nothing
:
unfoldrM :: Monad m => (a -> MaybeT m (b, a)) -> a -> m [b]
unfoldrM f x = fmap reverse $ unfoldrM' f x (return [ ])
where
-- unfoldrM' :: (a -> MaybeT m (b, a)) -> a -> m [b] -> m [b]
unfoldrM' f x ys = runMaybeT (f x) >>= \r -> case r of
Just (y, x') -> unfoldrM' f x' (fmap (y:) ys)
Nothing -> ys
f :: [Int] -> MaybeT IO (Int, [Int])
f (x: xs) = (lift . effectful $ x) >>= \y ->
if condition y
then MaybeT (return Nothing)
else lift . return $ (y, xs)
— Which got me thinking: "What if I used Either
instead, then unwrapped the Left
result?"
This line of consideration led me to Control.Monad.Except
and then to the idea that I should
consider the desired result to be the exception in the control flow.
exceptful :: Int -> ExceptT Int IO ()
exceptful n = do
r <- lift (effectful n)
if condition r
then throwError r
else return ()
loop' xs = fmap (fromRight ())
$ runExceptT (traverse_ exceptful xs `catchError` (lift . final))
λ loop' [1..10]
Effect: 1
Effect: 2
Effect: 3
Result: 3
What I think about this solution is that it is awful. First, it is counter-intuitive to use the
left side as the actual result carrier, second, this code is so much more complex than the
recursive loop
that I started with.
What can be done?
回答1:
I like to model these kinds of tasks as functions involving effectful streams. The streaming package is good for that, as it provides an api quite similar to that of conventional, pure lists. (That said, the Functor
/ Applicative
/ Monad
instances are a bit different: they work by Stream
"concatenation", not by exploring all combinations like in pure lists.)
For example:
import Streaming
import qualified Streaming.Prelude as S
loop :: Int -> (a -> Bool) -> IO a -> IO (Maybe a)
loop limit condition = S.head_ . S.filter condition . S.take limit . S.repeatM
Using the repeatM, take, filter and head_ functions from "streaming".
Or, if we have an effectful function and a list of values:
loop :: (b -> Bool) -> (a -> IO b) -> [a] -> IO (Maybe b)
loop condition effectful = S.head_ . S.filter condition . S.mapM effectful . S.each
Using each and mapM from "streaming".
If we want to perform a final effectful action:
loop :: (b -> IO ()) -> (b -> Bool) -> (a -> IO b) -> [a] -> IO ()
loop final condition effectful =
S.mapM_ final . S.take 1 . S.filter condition . S.mapM effectful . S.each
Using mapM_ from "streaming".
回答2:
There is one base
class that you are forgetting, my friend, and that is Alternative
. Consider the following definitions:
loop :: Alternative m => [m Int] -> m Int
loop = foldr (<|>) empty
effectful' :: Int -> IO Int
effectful' n = effectful n <* if condition n then return () else empty
Now surely you can see where it is going:
λ loop (effectful' <$> [1..10]) >>= final
Effect: 1
Effect: 2
Effect: 3
Result: 3
You can even have an infinite list of alternatives here; if there is a guarantee that eventually one of them will not be empty
, the whole loop is well-defined.
来源:https://stackoverflow.com/questions/57347510/what-recursion-scheme-should-i-use-to-repeat-an-effectful-action-until-its-resul