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.