Home > Software design >  Does haskell have an equivalent to rust's if let pattern = mightMatch syntax?
Does haskell have an equivalent to rust's if let pattern = mightMatch syntax?

Time:12-21

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.

  • Related