Действия ввода-вывода в качестве функторов
К настоящему моменту вы изучили, каким образом многие типы (если быть точным, конструкторы типов) являются экземплярами класса Functor: [] и Maybe, Either a, равно как и тип Tree, который мы создали в главе 7. Вы видели, как можно отображать их с помощью функций на всеобщее благо. Теперь давайте взглянем на экземпляр типа IO.
Если какое-то значение обладает, скажем, типом IO String, это означает, что перед нами действие ввода-вывода, которое выйдет в реальный мир и получит для нас некую строку, которую затем вернёт в качестве результата. Мы можем использовать запись <– в синтаксисе do для привязывания этого результата к имени. В главе 8 мы говорили о том, что действия ввода-вывода похожи на ящики с маленькими ножками, которые выходят наружу и приносят нам какое-то значение из внешнего мира. Мы можем посмотреть, что они принесли, но после просмотра нам необходимо снова обернуть значение в тип IO. Рассматривая эту аналогию с ящиками на ножках, вы можете понять, каким образом тип IO действует как функтор.
Давайте посмотрим, как же это тип IO является экземпляром класса Functor… Когда мы используем функцию fmap для отображения действия ввода-вывода с помощью функции, мы хотим получить обратно действие ввода-вывода, которое делает то же самое, но к его результирующему значению применяется наша функция. Вот код:
instance Functor IO where
fmap f action = do
result <– action
return (f result)
Результатом отображения действия ввода-вывода с помощью чего-либо будет действие ввода-вывода, так что мы сразу же используем синтаксис do для склеивания двух действий и создания одного нового. В реализации для метода fmap мы создаём новое действие ввода-вывода, которое сначала выполняет первоначальное действие ввода-вывода, давая результату имя result. Затем мы выполняем return (f result). Вспомните, что return – это функция, создающая действие ввода-вывода, которое ничего не делает, а только возвращает что-либо в качестве своего результата.
Действие, которое производит блок do, будет всегда возвращать результирующее значение своего последнего действия. Вот почему мы используем функцию return, чтобы создать действие ввода-вывода, которое в действительности ничего не делает, а просто возвращает применение f result в качестве результата нового действия ввода-вывода. Взгляните на этот кусок кода:
main = do
line <– getLine
let line' = reverse line
putStrLn $ "Вы сказали " ++ line' ++ " наоборот!"
putStrLn $ "Да, вы точно сказали " ++ line' ++ " наоборот!"
У пользователя запрашивается строка, и мы отдаём её обратно пользователю, но в перевёрнутом виде. А вот как можно переписать это с использованием функции fmap:
main = do
line <– fmap reverse getLine
putStrLn $ "Вы сказали " ++ line ++ " наоборот!"
putStrLn $ "Да, вы точно сказали " ++ line ++ " наоборот!"
Так же как можно отобразить Just "уфф" с помощью отображения fmap reverse, получая Just "ффу", мы можем отобразить и функцию getLine с помощью отображения fmap reverse. Функция getLine – это действие ввода-вывода, которое имеет тип IO String, и отображение его с помощью функции reverse даёт нам действие ввода-вывода, которое выйдет в реальный мир и получит строку, а затем применит функцию reverse к своему результату. Таким же образом, как мы можем применить функцию к тому, что находится внутри коробки Maybe, можно применить функцию и к тому, что находится внутри коробки IO, но она должна выйти в реальный мир, чтобы получить что-либо. Затем, когда мы привязываем результат к имени, используя запись <–, имя будет отражать результат, к которому уже применена функция reverse.
Действие ввода-вывода fmap (++"!") getLine ведёт себя в точности как функция getLine, за исключением того, что к её результату всегда добавляется строка "!" в конец!
Если бы функция fmap работала только с типом IO, она имела бы тип fmap :: (a –> b) –> IO a –> IO b. Функция fmap принимает функцию и действие ввода-вывода и возвращает новое действие ввода-вывода, похожее на старое, за исключением того, что к результату, содержащемуся в нём, применяется функция.
Предположим, вы связываете результат действия ввода-вывода с именем лишь для того, чтобы применить к нему функцию, а затем даёте очередному результату какое-то другое имя, – в таком случае подумайте над использованием функции fmap. Если вы хотите применить несколько функций к некоторым данным внутри функтора, то можете объявить свою функцию на верхнем уровне, создать анонимную функцию или, в идеале, использовать композицию функций:
import Data.Char
import Data.List
main = do
line <– fmap (intersperse '-' . reverse . map toUpper) getLine
putStrLn line
Вот что произойдёт, если мы сохраним этот код в файле fmapping_io.hs, скомпилируем, запустим и введём "Эй, привет":
$ ./fmapping_io
Эй, привет
Т-Е-В-И-Р-П- -,-Й-Э
Выражение intersperse '-' . reverse . map toUpper берёт строку, отображает её с помощью функции toUpper, применяет функцию reverse к этому результату, а затем применяет к нему выражение intersperse '-'. Это более красивый способ записи следующего кода:
(xs –> intersperse '-' (reverse (map toUpper xs)))