Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell: Run two monads, keep the result of the first one

Tags:

haskell

monads

Playing with Haskell and now I try to create a function like

keepValue :: (Monad m) => m a -> (a -> m b) -> m a

with following semantic: it should apply monad value to a function, which return the second monad, and keep the result of the first monad, but the effect of the second one

I have a working function in case of Maybe monad:

keepValueMaybe :: Maybe a -> (a -> Maybe b) -> Maybe a

keepValue ma f = case ma >>= f of
    Nothing -> Nothing
    Just _ -> ma

So if the first value is Nothing, the function is not run (so no second side-effect), but if the first value is Just, then, the function is run (with side effect). I keep effect of the second computation (e.g., Nothing makes the whole expression Nothing), but the original value.

Now I wonder. Can it work for any monad?

It looks kinda built-in >>, but I couldn't find anything in standard library.

like image 562
dmzkrsk Avatar asked Feb 15 '17 06:02

dmzkrsk


2 Answers

Let's walk through this!

keepValue :: Monad m => m a -> (a -> m b) -> m a
keepValue ma f = _

So what do we want keepValue to do? Well, the first thing we should do is use ma, so we can connect it to f.

keepValue :: Monad m => m a -> (a -> m b) -> m a
keepValue ma f = do
  a <- ma
  _

Now we have a value va of type a, so we can pass it to f.

keepValue :: Monad m => m a -> (a -> m b) -> m a
keepValue ma f = do
  va <- ma
  vb <- f va
  _

And finally, we want to produce va, so we can just do that:

keepValue :: Monad m => m a -> (a -> m b) -> m a
keepValue ma f = do
  va <- ma
  vb <- f va
  return va

This is how I'd walk through writing the first draft of any monadic function like this. Then, I'd clean it up. First, some small things: since Applicative is a superclass of Monad, I prefer pure to return; we didn't use vb; and I'd drop the v in the names. So for a do-notation based version of this function, I think the best option is

keepValue :: Monad m => m a -> (a -> m b) -> m a
keepValue ma f = do
  a <- ma
  _ <- f a
  pure a

Now, however, we can start to make the implementation better. First, we can replace _ <- f va with an explicit call to (>>):

keepValue :: Monad m => m a -> (a -> m b) -> m a
keepValue ma f = do
  a <- ma
  f a >> pure a

And now, we can apply a simplification. You may know that we can always replace (>>=) plus pure/return with fmap/(<$>): any of pure . f =<< ma, ma >>= pure . f, or do a <- ma ; pure $ f a (all of which are equivalent) can be replaced by f <$> ma. However, the Functor type class has another, less-well-known method, (<$):

(<$) :: a -> f b -> f a
Replace all locations in the input with the same value. The default definition is fmap . const, but this may be overridden with a more efficient version.

So we have a similar replacement rule for (<$): we can always replace ma >> pure b or do ma ; pure b with b <$ ma. This gives us

keepValue :: Monad m => m a -> (a -> m b) -> m a
keepValue ma f = do
  a <- ma
  a <$ f a

And I think this is the shortest reasonable version of this function! There aren't any nice point-free tricks to make this cleaner; one indicator of that is the multiple use of a on the second line of the do block.


Incidentally, a terminology note: you're running two monadic actions, or two monadic values; you're not running *"two monads". A monad is something like Maybe – a type constructor which supports (>>=) and return. Don't mix the values up with the types – this sort of terminological distinction helps keep things clearer!

like image 81
Antal Spector-Zabusky Avatar answered Oct 20 '22 11:10

Antal Spector-Zabusky


This structure looks a lot like the definition of >>=/>> for Monad Maybe.

case foo of
    Nothing -> Nothing
    Just _ -> bar

foo >>= \_ -> bar
foo >> bar

so your original expression could be simplified to

ma >>= f >> ma

and this works for other monads.

However, I don't think this is actually what you want, as you can see ma occurring twice. Instead, take the value from the first ma >>= bind, and carry it through to the end of the computation.

keepValue ma f =
    ma >>= \a ->
    f a >>
    return a

or in do-notation

keepValue ma f = do
    a <- ma
    f a
    return a
like image 1
ephemient Avatar answered Oct 20 '22 11:10

ephemient