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 ()
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.
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With