Binding functions that take multiple arguments

2019-03-24 13:57发布

问题:

After reading some very basic haskell now I know how to "chain" monadic actions using bind, like:

echo = getLine >>= putStrLn

(>>=) operator is very handy in this fashion, but what if I want to chain monadic actions (or functors) that take multiple arguments?

Given that (>>=) :: m a -> (a -> m b) -> m b it seems like (>>=) can supply only one argument.

For example, writeFile takes two arguments (a FilePath and the contents). Suppose I have a monadic action that returns a FilePath, and another action that returns a String to write. How can I combine them with writeFile, without using the do-notation, but in a general way?

Is there any function with the type: m a -> m b -> (a -> b -> m c) -> m c that can do this?

回答1:

TL;DR:

writeFile <$> getFilename <*> getString >>= id   :: IO ()

Monads are Applicative

Since ghc 7.10 every Monad (including IO) is also an Applicative, but even before that, you could make an Applicative out of any Monad using the equivalent of

import Control.Applicative -- not needed for ghc >= 7.10

instance Applicative M where
  pure x = return x
  mf <*> mx = do
    f <- mf
    x <- mx
    return (f x)

And of course IO is a functor, but Control.Applicative gives you <$> which can be defined as f <$> mx = fmap f mx.

Use Applicative to use functions that take as many arguments as you like

<$> and <*> let you use pure functions f over arguments produced by an Applicative/Monadic computation, so if f :: String -> String -> Bool and getFileName, getString :: IO String then

f <$> getFileName <*> getString :: IO Bool

Similarly, if g :: String -> String -> String -> Int, then

g <$> getString <*> getString <*> getString :: IO Int

From IO (IO ()) to IO ()

That means that

writeFile <$> getFilename <*> getString :: IO (IO ())

but you need something of type IO (), not IO (IO ()), so we need to either use join :: Monad m => m (m a) -> m a as in Xeo's comment, or we need a function to take the monadic result and run it, i.e. of type (IO ()) -> IO () to bind it with. That would be id then, so we can either do

join $ writeFile <$> getFilename <*> getString :: IO ()

or

writeFile <$> getFilename <*> getString >>= id :: IO ()


回答2:

It's much easier to use do notation for this, rather than asking for a combinator

action1 :: MyMonad a
action2 :: MyMonad b
f :: a -> b -> MyMonad c

do
    x <- action1
    y <- action2
    f x y


回答3:

This typechecks:

import System.IO

filepath :: IO FilePath
filepath = undefined

someString :: IO String
someString = undefined

testfun = filepath   >>= (\fp -> 
          someString >>= (\str -> 
          writeFile fp str  ))

But I feel using do notation is more readable.