Search code examples
haskellpointfree

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


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 ()

Solution

  • 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.