Learn You a Haskell for Great Good! - Miran Lipovaca [98]
instance Functor IO where
fmap f action = do
result <- action
return (f result)
The result of mapping something over an I/O action will be an I/O action, so right off the bat, we use the do syntax to glue two actions and make a new one. In the implementation for fmap, we make a new I/O action that first performs the original I/O action and calls its result result. Then we do return (f result). Recall that return is a function that makes an I/O action that doesn’t do anything but only yields something as its result.
The action that a do block produces will always yield the result value of its last action. That’s why we use return to make an I/O action that doesn’t really do anything; it just yields f result as the result of the new I/O action. Check out this piece of code:
main = do line <- getLine
let line' = reverse line
putStrLn $ "You said " ++ line' ++ " backwards!"
putStrLn $ "Yes, you said " ++ line' ++ " backwards!"
The user is prompted for a line, which we give back, but reversed. Here’s how to rewrite this by using fmap:
main = do line <- fmap reverse getLine
putStrLn $ "You said " ++ line ++ " backwards!"
putStrLn $ "Yes, you really said " ++ line ++ " backwards!"
Just as we can fmap reverse over Just "blah" to get Just "halb", we can fmap reverse over getLine. getLine is an I/O action that has a type of IO String, and mapping reverse over it gives us an I/O action that will go out into the real world and get a line and then apply reverse to its result. In the same way that we can apply a function to something that’s inside a Maybe box, we can apply a function to what’s inside an IO box, but it must go out into the real world to get something. Then when we bind it to a name using <-. The name will reflect the result that already has reverse applied to it.
The I/O action fmap (++"!") getLine behaves just like getLine, except that its result always has "!" appended to it!
If fmap were limited to IO, its type would be fmap :: (a -> b) -> IO a -> IO b. fmap takes a function and an I/O action and returns a new I/O action that’s like the old one, except that the function is applied to its contained result.
If you ever find yourself binding the result of an I/O action to a name, only to apply a function to that and call that something else, consider using fmap. If you want to apply multiple functions to some data inside a functor, you can declare your own function at the top level, make a lambda function, or, ideally, use function composition:
import Data.Char
import Data.List
main = do line <- fmap (intersperse '-' . reverse . map toUpper) getLine
putStrLn line
Here’s what happens if we run this with the input hello there:
$ ./fmapping_io
hello there
E-R-E-H-T- -O-L-L-E-H
The intersperse '-' . reverse . map toUpper function takes a string, maps toUpper over it, applies reverse to that result, and then applies intersperse '-' to that result. It’s a prettier way of writing the following:
(\xs -> intersperse '-' (reverse (map toUpper xs)))
Functions As Functors
Another instance of Functor that we’ve been dealing with all along is (->) r. But wait! What the heck does (->) r mean? The function type r -> a can be rewritten as (->) r a, much like we can write 2 + 3 as (+) 2 3. When we look at it as (->) r a, we can see (->) in a slightly different light. It’s just a type constructor that takes two type parameters, like Either.
But remember that a type constructor must take exactly one type parameter so it can be made an instance of Functor. That’s why we can’t make (->) an instance of Functor; however, if we partially apply it to (->) r, it doesn’t pose any problems. If the syntax allowed for type constructors to be partially applied with sections (like we can partially apply + by doing (2+), which is the same as (+) 2), we could write (->) r as (r ->).
How are functions functors?