In rust, we can say e.g.
if Some(inner_val) = option { ... }
Does haskell have an equivalent?
I'll give a little context in case this seems like a silly thing to do (and feel free to tell me it is). I was reading the conduit readme and doing its exercises as I went along. One instructs the reader to "Implement a peek function that gets the next value from upstream, if available, and then puts it back on the stream." I did so as follows:
peek :: Monad m => ConduitT a o m (Maybe a)
peek = do
mx <- await
case mx of
Nothing -> return mx
Just x -> do
leftover x
return mx
I don't like that I had to say return mx
in both arms. One could easily imagine cases where a lot more work is shared between arms than a simple return
. I want a way to conditionally do the unshared work and then continue onward. I could say
peek2 :: Monad m => ConduitT a o m (Maybe a)
peek2 = do
mx <- await
when
(isJust mx)
( do
let Just x = mx
leftover x
)
return mx
but that matches on mx
twice, not to mention that the compiler gets angry with me. Moreover, it's specific to Maybe
, but I might want to match an arbitrary type and only do something when it's a specific shape. What's the idiomatic way to do this?
CodePudding user response:
I don't see why you have to mention return mx
twice. This is just equivalent to:
peek :: Monad m => ConduitT a o m (Maybe a)
peek = do
mx <- await
case mx of
Just x -> leftover x
_ -> pure ()
return mx
The return mx
is thus part of the "outer do
block" if you want. The pure ()
acts as a no-op. This is sometimes named skip
or yield
.
CodePudding user response:
When you want to match on a Maybe
being Just
in particular (or a few other cases, like an Either
being Right
), as you do in your example, you can use traverse_
:
import Data.Conduit
import Data.Foldable (traverse_)
peek :: Monad m => ConduitT a o m (Maybe a)
peek = do
mx <- await
traverse_ leftover mx
return mx
If you had more than a single function you wanted to call on the inside, you could use for_
(which is equivalent to flip traverse_
) instead and write out more steps in a convenient lambda, like this:
import Data.Conduit
import Data.Foldable (for_)
peek :: Monad m => ConduitT a o m (Maybe a)
peek = do
mx <- await
for_ mx $ \x ->
foo x
bar x
baz x
return mx
CodePudding user response:
Remember that return mx
is not just a statement, it's a value. A case
expression (like an if
expression) is also a value, as a whole; it's equal to whichever value corresponds to the matching condition. That means there has to be a value for every possible branch. Leaving one out doesn't make any sense.
What you want to say is something like "if mx
is Just x
then do leftover x
, it it is Nothing
then do nothing; after that return mx
". Haskell is totally fine with you saying that and it needs no special syntax to do so; you just have to actually say it by providing a value in the Nothing
arm of the case
that corresponds to "do nothing". Fortunately this is easy when you're working in a monad; pure ()
or return ()
(they're equivalent, but pure
is the more modern way to phrase it) is a monadic value with no side effects and a boring return value.
peek :: Monad m => ConduitT a o m (Maybe a)
peek = do
mx <- await
case mx of
Nothing -> return ()
Just x -> do
leftover x
return mx
Now you might wonder how when
manages to avoid requiring you to explicitly provide the "do nothing" case. It's nothing magical, when
is simply implemented like this:
when :: (Applicative f) => Bool -> f () -> f ()
when p s = if p then s else pure ()
It's simply an ordinary function that encapsulates the pattern. when
provides the pure ()
do-nothing case (and indeed the whole if
expression) so the caller doesn't have to. If you find yourself wanting the "when Just
x do something with x, otherwise do nothing" pattern repeatedly, just do what you always do to avoid repetition: factor out the bits that never change into a function that hardcodes those bits and takes the bits that do change as parameters:
whenJust :: Applicative f => Maybe a -> (a -> f ()) -> f ()
whenJust Nothing _ = pure ()
whenJust (Just x) f = f x
when
just takes a simple f ()
argument to say what to do if the boolean is true, because booleans contain no useful data other than the true/false distinction. Maybe
is different in that the Just
case actually has a value we almost certainly want to use, so to encapsulate that whole arm of the case to pass to whenJust
we need a function that will receive the value that was in the Just
. Using it looks like this:
peek :: Monad m => ConduitT a o m (Maybe a)
peek = do
mx <- await
whenJust mx leftover
return mx
Or if your case for Just
was more complicated, it could look like:
peek :: Monad m => ConduitT a o m (Maybe a)
peek = do
mx <- await
whenJust mx $ \x -> do
a <- step1 x
b <- step2 x
etc
return mx
And indeed if I search Hoogle for "whenJust" I see that a number of other package authors have had the same idea; if any of those are suitable for you to depend on you could reuse one of their definitions, but it's so small and obvious that I wouldn't worry about duplication if you prefer to just write your own definition rather than add a dependency.
And of course you can apply similar patterns to Either
or any other type where you might want to encapsulate a specific pattern of pattern-match, so that you don't have to repeat it in full every time.
Another way you can look at this is to remember that Maybe
is Traversable
and Foldable
; Joseph Sible's answer covers that. It would probably be better practice to use traverse_
or for_
for this (or define whenJust = for_
if you want the more informative special-case name). But this answer hopefully will help you think about more general cases of this issue, and how to make your own combinators when they don't boil down to existing standard functions.