Pergunta

I'm having trouble understanding how this Haskell expression works:

import Control.Monad
import System.IO
(forM_ [stdout, stderr] . flip hPutStrLn) "hello world"

What is the . flip hPutStrLn part doing exactly? The type signatures seem complicated:

ghci> :type flip
flip :: (a -> b -> c) -> b -> a -> c
ghci> :type (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
ghci> :type (. flip)
(. flip) :: ((b -> a -> c1) -> c) -> (a -> b -> c1) -> c
ghci> :type (. flip hPutStrLn)
(. flip hPutStrLn) :: ((Handle -> IO ()) -> c) -> String -> c

What becomes the left and right operands of the (.) operator as the expression is evaluated?

Another way to putting my question is, how does the left part of the expression at the top end up with a type signature like this:

(forM_ [stdout, stderr] . flip hPutStrLn) :: String -> IO ()
Foi útil?

Solução

The left and right operands of (.) are

forM_ [stdout, stderr]

and

flip hPutStrLn

respectively.

The type of hPutStrLn is

hPutStrLn :: Handle -> String -> IO ()

so flip hPutStrLn has type

flip hPutStrLn :: String -> Handle -> IO ()

As the type system tells you, flip is a combinator that swaps the order of another function’s arguments. Specified in the abstract

flip       :: (a -> b -> c) -> b -> a -> c
flip f x y =  f y x

From ghci you already know that the type of (. flip hPutStrLn) is

ghci> :type (. flip hPutStrLn)
(. flip hPutStrLn) :: ((Handle -> IO ()) -> c) -> String -> c

Working from the other direction, the type of the left side is

ghci> :type forM_ [stdout, stderr]
forM_ [stdout, stderr] :: Monad m => (Handle -> m b) -> m ()

Observe how the types fit together.

(. flip hPutStrLn)     ::            ((Handle -> IO ()) -> c   ) -> String -> c
forM_ [stdout, stderr] :: Monad m =>  (Handle -> m  b ) -> m ()

Combining the two (calling the first with the second) gives

ghci> :type forM_ [stdout, stderr] . flip hPutStrLn
forM_ [stdout, stderr] . flip hPutStrLn :: String -> IO ()

In your question, the result of the composition is applied to a String, and that produces an I/O action that yields (), i.e., we are mainly interested in its side effects of writing to the standard output and error streams.

With point-free style such as the definition in your question, the programmer defines more complex functions in terms of smaller, simpler functions by composing them with (.). The flip combinator is useful for reordering arguments so as to make repeated partial applications fit together.

Outras dicas

flip reverses the arguments of an input function, i.e.:

flip hPutStrLn == \a b -> hPutStrLn b a

The . is a function composition operator (or infix function), which lets you nicely chain functions together. Without this operator your expression can be rewritten as follows:

forM_ [stdout, stderr] ((flip hPutStrLn) "hello world")

which is the same as:

forM_ [stdout, stderr] (flip hPutStrLn "hello world")

or, using the application operator:

forM_ [stdout, stderr] $ flip hPutStrLn "hello world"

Concerning the . operands question. Consider the type signature of .:

(.) :: (b -> c) -> (a -> b) -> a -> c

You can view it as a function from 3 arguments: a function b -> c, a function a -> b and a value a - to a resulting value c, but also due to Currying, you can see it as a function from two arguments: b -> c and a -> b - to a result function of type a -> c. And this is what happens in your example: you pass two functions (forM_ [stdout, stderr] and flip hPutStrLn, which are themselves results of currying) to . and get a function of type String -> IO () as a result.

Here's a somewhat shorter derivation of that type (as hinted in the 2nd part of Nikita Volkov's answer).

Knowing (.) :: (b -> c) -> (a -> b) -> a -> c and (f . g) x = f (g x), so that

(f . g) :: a -> c      where  g :: (a -> b)  and  f :: (b -> c)

(the b in a -> b and b -> c disappears after performing the unification, giving the a -> c type) and since

flip hPutStrLn         ::    String -> (Handle -> IO ())             -- g
forM_ [stdout, stderr] :: (Monad m) => (Handle -> m  b ) -> m ()     -- f

(we put parentheses around the Handle -> IO () in the first type, using the fact that in types -> is right associative), the resulting type of composing the second with the first (via the function composition operator) is

(Monad m) => String -> m ()     where  m ~ IO  and b ~ ()
                                (found by unification of
                                        Handle -> IO ()  and
                                        Handle -> m  b       )

i.e. String -> IO ().

The order of arguments for (.) takes a little getting used to; it fires up its second argument function first, and then uses the result to call its first argument function. If we import Control.Arrow we can then use >>> operator which is like (.) in reverse, with functions: (f . g) x == (g >>> f) x.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top