Миран Липовача - Изучай Haskell во имя добра!
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)))
Функции в качестве функторов
Другим экземпляром класса Functor, с которым мы всё время имели дело, является (–>) r. Стойте!.. Что, чёрт возьми, означает (–>) r? Тип функции r –> a может быть переписан в виде (–>) r a, так же как мы можем записать 2 + 3 в виде (+) 2 3. Когда мы воспринимаем его как (–>) r a, то (–>) представляется немного в другом свете. Это просто конструктор типа, который принимает два параметра типа, как это делает конструктор Either.
Но вспомните, что конструктор типа должен принимать в точности один параметр типа, чтобы его можно было сделать экземпляром класса Functor. Вот почему нельзя сделать конструктор (–>) экземпляром класса Functor; однако, если частично применить его до (–>) r, это не составит никаких проблем. Если бы синтаксис позволял частично применять конструкторы типов с помощью сечений – подобно тому как можно частично применить оператор +, выполнив (2+), что равнозначно (+) 2, – вы могли бы записать (–>) r как (r –>).
Каким же образом функции выступают в качестве функторов? Давайте взглянем на реализацию, которая находится в модуле Control.Monad.Instances.
instance Functor ((–>) r) where
fmap f g = (x –> f (g x))
Сначала подумаем над типом метода fmap:
fmap :: (a –> b) –> f a –> f b
Далее мысленно заменим каждое вхождение идентификатора f, являющегося ролью, которую играет наш экземпляр функтора, выражением (–>) r. Это позволит нам понять, как функция fmap должна вести себя в отношении данного конкретного экземпляра. Вот результат:
fmap :: (a –> b) –> ((–>) r a) –> ((–>) r b)
Теперь можно записать типы (–>) r a и (–>) r b в инфиксном виде, то есть r –> a и r –> b, как мы обычно поступаем с функциями:
fmap :: (a –> b) –> (r –> a) –> (r –> b)
Хорошо. Отображение одной функции с помощью другой должно произвести функцию, так же как отображение типа Maybe с помощью функции должно произвести тип Maybe, а отображение списка с помощью функции – список. О чём говорит нам предыдущий тип? Мы видим, что он берёт функцию из a в b и функцию из r в a и возвращает функцию из r в b. Напоминает ли это вам что-нибудь? Да, композицию функций!.. Мы присоединяем выход r –> a ко входу a –> b, чтобы получить функцию r –> b, чем в точности и является композиция функций. Вот ещё один способ записи этого экземпляра:
instance Functor ((–>) r) where
fmap = (.)
Код наглядно показывает, что применение функции fmap к функциям – это просто композиция функций.
В исходном коде импортируйте модуль Control.Monad.Instances, поскольку это модуль, где определён данный экземпляр, а затем загрузите исходный код и попробуйте поиграть с отображением функций:
ghci> :t fmap (*3) (+100)
fmap (*3) (+100) :: (Num a) => a –> a
ghci> fmap (*3) (+100) 1
303
ghci> (*3) `fmap` (+100) $ 1
303
ghci> (*3) . (+100) $ 1
303
ghci> fmap (show . (*3)) (*100) 1
"300"
Мы можем вызывать fmap как инфиксную функцию, чтобы сходство с оператором . было явным. Во второй строке ввода мы отображаем (+100) с помощью (*3), что даёт функцию, которая примет ввод, применит к нему (+100), а затем применит к этому результату (*3). Затем мы применяем эту функцию к значению 1.
Как и все функторы, функции могут восприниматься как значения с контекстами. Когда у нас есть функция вроде (+3), мы можем рассматривать значение как окончательный результат функции, а контекстом является то, что мы должны применить эту функцию к чему-либо, чтобы получить результат. Применение fmap (*3) к (+100) создаст ещё одну функцию, которая действует так же, как (+100), но перед возвратом результата к этому результату будет применена функция (*3).
Тот факт, что функция fmap является композицией функций при применении к функциям, на данный момент не слишком нам полезен, но, по крайней мере, он вызывает интерес. Это несколько меняет наше сознание и позволяет нам увидеть, как сущности, которые действуют скорее как вычисления, чем как коробки (IO и (–>) r), могут быть функторами. Отображение вычисления с помощью функции возвращает тот же самый тип вычисления, но результат этого вычисления изменён функцией.
Перед тем как перейти к законам, которым должна следовать fmap, давайте ещё раз задумаемся о типе fmap:
fmap :: (a –> b) –> f a –> f b
Если помните, введение в каррированные функции в главе 5 началось с утверждения, что все функции в языке Haskell на самом деле принимают один параметр. Функция a –> b –> c в действительности берёт только один параметр типа a, после чего возвращает функцию b –> c, которая принимает один параметр типа b и возвращает значение типа c. Вот почему вызов функции с недостаточным количеством параметров (её частичное применение) возвращает нам обратно функцию, принимающую несколько параметров, которые мы пропустили (если мы опять воспринимаем функции так, как если бы они принимали несколько параметров). Поэтому a –> b –> c можно записать в виде a –> (b –> c), чтобы сделать каррирование более очевидным.
Аналогичным образом, записав fmap :: (a –> b) –> (f a –> f b), мы можем воспринимать fmap не как функцию, которая принимает одну функцию и значение функтора и возвращает значение функтора, но как функцию, которая принимает функцию и возвращает новую функцию, которая такая же, как и прежняя, за исключением того, что она принимает значение функтора в качестве параметра и возвращает значение функтора в качестве результата. Она принимает функцию типа a –> b и возвращает функцию типа f a –> f b. Это называется «втягивание функции». Давайте реализуем эту идею, используя команду :t в GHCi:
Откройте для себя мир чтения на siteknig.com - месте, где каждая книга оживает прямо в браузере. Здесь вас уже ждёт произведение Миран Липовача - Изучай Haskell во имя добра!, относящееся к жанру Программирование. Никаких регистраций, никаких преград - только вы и история, доступная в полном формате. Наш литературный портал создан для тех, кто любит комфорт: хотите читать с телефона - пожалуйста; предпочитаете ноутбук - идеально! Все книги открываются моментально и представлены полностью, без сокращений и скрытых страниц. Каталог жанров поможет вам быстро найти что-то по настроению: увлекательный роман, динамичное фэнтези, глубокую классику или лёгкое чтение перед сном. Мы ежедневно расширяем библиотеку, добавляя новые произведения, чтобы вам всегда было что открыть "на потом". Сегодня на siteknig.com доступно более 200000 книг - и каждая готова стать вашей новой любимой. Просто выбирайте, открывайте и наслаждайтесь чтением там, где вам удобно.

