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.