Действие, которое производит блок 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
Эй, привет
Т-Е-В-И-Р-П- -,-Й-Э
Выражение intersperse '-' . reverse . map toUpper
берёт строку, отображает её с помощью функции toUpper
, применяет функцию reverse
к этому результату, а затем применяет к нему выражение intersperse '-'
. Это более красивый способ записи следующего кода:
(\xs –> intersperse '-' (reverse (map toUpper xs)))
Функции в качестве функторов
Другим экземпляром класса Functor
, с которым мы всё время имели дело, является (–>) r
. Стойте!.. Что, чёрт возьми, означает (–>) r
? Тип функции r –> a
может быть переписан в виде (–>) r a
, так же как мы можем записать 2 + 3
в виде (+) 2 3
. Когда мы воспринимаем его как (–>) r a
, то (–>)
представляется немного в другом свете. Это просто конструктор типа, который принимает два параметра типа, как это делает конструктор Either
.
Но вспомните, что конструктор типа должен принимать в точности один параметр типа, чтобы его можно было сделать экземпляром класса Functor
. Вот почему нельзя сделать конструктор (–>)
экземпляром класса Functor
; однако, если частично применить его до (–>) r
, это не составит никаких проблем. Если бы синтаксис позволял частично применять конструкторы типов с помощью сечений – подобно тому как можно частично применить оператор +,
выполнив (2+)
, что равнозначно (+)
2
, – вы могли бы записать (–>)
r
как (r
–>)
.