3

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?

Ignat Insarov
  • 4,660
  • 18
  • 37
  • 1
    You can use `whileM`: https://hackage.haskell.org/package/extra-1.6.17/docs/Control-Monad-Extra.html#v:whileM – Willem Van Onsem Aug 04 '19 at 14:48
  • @WillemVanOnsem Awesome, why is this not in base? – Ignat Insarov Aug 04 '19 at 15:13
  • 1
    @IgnatInsarov it's not clear there's a benefit to having every combinator definable in 5 lines or less in the standard library as opposed to redefining it when you need it. You have to weigh that against the effort to look it up when you write code using it, and when you read code using it. – Li-yao Xia Aug 04 '19 at 16:05
  • @Li-yaoXia To define every possible combinator is one thing. To have an equivalent of a simple loop, one of the few most fundamental control structures from somewhere like the 60s, is another thing altogether. After all, we have `<-` and `if` as keywords even. – Ignat Insarov Aug 04 '19 at 20:21
  • 1
    @IgnatInsarov: I personally to some extend like the fact that `whileM` and `forM`s are not very "close" to the language. Since this helps to think more "declaratively" over a problem. Every now and then I see some users that aim to "translate" imperative code in Haskell: they use `MVar`s to make mutable variables. Although this might work, it is definitely not much "idiomatic". – Willem Van Onsem Aug 04 '19 at 20:58
  • 1
    For example `if ... then ... else` is not necessary, one can use the [`ifThenElse` function](https://hackage.haskell.org/package/utility-ht-0.0.14/docs/Data-Bool-HT.html#v:ifThenElse). Haskell has suprisingly few real keywords (as in tokens that have to be defined in the language, and can not be defined through the library itself). – Willem Van Onsem Aug 04 '19 at 21:02
  • You say "fundamental control structure" I say "just a particular case of recursion". – Li-yao Xia Aug 05 '19 at 01:31

2 Answers2

2

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".

danidiaz
  • 26,936
  • 4
  • 45
  • 95
0

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.

Ignat Insarov
  • 4,660
  • 18
  • 37
  • `(<|>)` for `IO` is dangerous because it hides exceptions. – Li-yao Xia Aug 05 '19 at 17:44
  • Some possible simplifications: `foldr (<|>) empty` is `Data.Foldable.asum`. `if condition n then return () else empty` could be implemented using `Control.Monad.guard`. Alas, your solution will throw an `IOException` if no element satisfies the condition. `IO` has a strange `Alternative` instance for which `empty` throws an exception and `<|>` catches exceptions thrown by the first operand. – danidiaz Aug 05 '19 at 17:45
  • Related: https://stackoverflow.com/questions/48450826/what-is-the-purpose-of-instance-alternative-io/48452048#48452048 – danidiaz Aug 07 '19 at 18:34
  • @Li-yaoXia Well turns out it only hides IO errors, which is just a fraction of synchronous errors that are anyway generally safe to ignore. So I do not see what the danger is. – Ignat Insarov Aug 07 '19 at 19:34
  • @IgnatInsarov The danger is that if `effectful` throws an IO exception -- say, because it tried to open a file and failed -- the loop you have written here will not rethrow that exception, but instead blithely continue on to the next iteration exactly as if it were merely the case that the condition were not yet satisfied. – Daniel Wagner Aug 07 '19 at 21:03