Эта глава рассказывает о том, как объединять функции в цепочки, а также о том, как избавиться от круглых скобок.
Да, я не люблю круглые скобки. Они делают код визуально избыточным, к тому же нужно следить за симметрией скобок открывающих и закрывающих. Вспомним пример из главы про кортежи:
main :: IO ()
main =
putStrLn (patientEmail ( "63ab89d"
^ , "John Smith"
, "[email protected]"
, 59
))
^
Со скобками кортежа мы ничего сделать не можем, ведь они являются синтаксической частью кортежа. А вот скобки вокруг применения функции patientEmail
мне абсолютно не нравятся. К счастью, мы можем избавиться от них. Но прежде чем искоренять скобки, задумаемся вот о чём.
Если применение функции представляет собой выражение, не можем ли мы как-нибудь компоновать их друг с другом? Конечно можем, мы уже делали это много раз, вспомните:
main :: IO ()
main = putStrLn (checkLocalhost "173.194.22.100")
Здесь компонуются две функции, putStrLn
и checkLocalhost
, потому что тип выражения на выходе функции checkLocalhost
совпадает с типом выражения на входе функции putStrLn
. Схематично это можно изобразить так:
┌──────────────┐ ┌────────┐
String ->│checkLocalhost│-> String ->│putStrLn│-> ...
└──────────────┘ └────────┘
IP-адрес сообщение текст
об этом в нашем
IP-адресе терминале
Получается эдакий конвейер: на входе строка с IP-адресом, на выходе — сообщение в нашем терминале. Существует иной способ соединения двух функций воедино.
Взгляните:
main :: IO ()
main = putStrLn . checkLocalhost $ "173.194.22.100"
Необычно? Перед нами два новых стандартных оператора, избавляющие нас от лишних скобок и делающие наш код проще. Оператор .
— это оператор композиции функций (англ. function composition), а оператор $
— это оператор применения (англ. application operator). Эти операторы часто используют совместно друг с другом. И отныне мы будем использовать их чуть ли не в каждой главе.
Оператор композиции объединяет две функции воедино (или компонует их, англ. compose). Когда мы пишем:
putStrLn . checkLocalhost
происходит маленькая «магия»: две функции объединяются в новую функцию. Вспомним наш конвейер:
┌──────────────┐ ┌────────┐
String ->│checkLocalhost│-> String ->│putStrLn│-> ...
└──────────────┘ └────────┘
A B C
Раз нам нужно попасть из точки A
в точку C
, нельзя ли сделать это сразу? Можно, и в этом заключается суть композиции: мы берём две функции и объединяем их в третью функцию. Раз checkLocalhost
приводит нас из точки A
в точку B
, а функция putStrLn
— из точки B
в C
, тогда композиция этих двух функций будет представлять собой функцию, приводящую нас сразу из точки A
в точку C
:
┌─────────────────────────┐
String ->│checkLocalhost + putStrLn│-> ...
└─────────────────────────┘
A C
В данном случае знак +
не относится к конкретному оператору, я лишь показываю факт «объединения» двух функций в третью. Теперь-то нам понятно, почему в типе функции, в качестве разделителя, используется стрелка:
checkLocalhost :: String -> String
в нашем примере это:
checkLocalhost :: A -> B
Она показывает наше движение из точки A
в точку B
. Поэтому часто говорят о «функции из A
в B
». Так, о функции checkLocalhost
можно сказать как о «функции из String
в String
».
А оператор применения работает ещё проще. Без него код был бы таким:
main :: IO ()
main =
(putStrLn . checkLocalhost) "173.194.22.100"
объединённая функция аргумент
Но мы ведь хотели избавиться от круглых скобок, а тут они опять. Вот для этого и нужен оператор применения. Его схема проста:
FUNCTION $ ARGUMENT
вот эта применяется вот этому
функция к аргументу
Для нашей объединённой функции это выглядит так:
main :: IO ()
main =
putStrLn . checkLocalhost $ "173.194.22.100"
объединённая функция применяется
к этому аргументу
Теперь получился настоящий конвейер: справа в него «заезжает» строка и движется «сквозь» функции, а слева «выезжает» результат:
main = putStrLn . checkLocalhost $ "173.194.22.100"
<- <- <- аргумент
Чтобы было легче читать композицию, вместо оператора .
мысленно подставляем фразу «применяется после»:
putStrLn . checkLocalhost
эта применяется этой
функция после функции
То есть композиция правоассоциативна (англ. right-associative): сначала применяется функция справа, а затем — слева.
Ещё одно замечание про оператор применения функции. Он весьма гибок, и мы можем написать так:
main = putStrLn . checkLocalhost $ "173.194.22.100"
объединённая функция └─ её аргумент ─┘
а можем и так:
main = putStrLn $ checkLocalhost "173.194.22.100"
обычная └──────── её аргумент ────────┘
функция
Эти две формы, как вы уже поняли, эквивалентны. Я показываю это для того, чтобы вновь и вновь продемонстрировать вам, сколь гибко можно работать с данными и функциями в Haskell.
Красота композиции в том, что компоновать мы можем сколько угодно функций:
logWarn :: String -> String
logWarn rawMessage =
warning . correctSpaces . asciiOnly $ rawMessage
main :: IO ()
main = putStrLn $
logWarn "Province 'Gia Viễn' isn't on the map! "
Функция logWarn
готовит переданную ей строку для записи в журнал. Функция asciiOnly
готовит строку к выводу в нелокализованном терминале (да, в 2016 году такие всё ещё имеются), функция correctSpaces
убирает дублирующиеся пробелы, а функция warning
делает строку предупреждением (например, добавляет строку "WARNING: "
в начало сообщения). При запуске этой программы мы увидим:
WARNING: Province 'Gia Vi?n' isn't on the map!
Здесь мы объединили в «функциональный конвейер» уже три функции, безо всяких скобок. Вот как это получилось:
warning . correctSpaces . asciiOnly $ rawMessage
^
└── первая композиция ──┘
^
└────── вторая композиция ────────┘
аргумент
Первая композиция объединяет две простые функции, correctSpaces
и asciiOnly
. Вторая объединяет тоже две функции, простую warning
и объединённую, являющуюся результатом первой композиции.
Более того, определение функции logWarn
можно сделать ещё более простым:
logWarn :: String -> String
logWarn = warning . correctSpaces . asciiOnly
Погодите, но где же имя аргумента? А его больше нет, оно нам не нужно. Ведь мы знаем, что применение функции можно легко заменить внутренним выражением функции. А раз так, выражение logWarn
может быть заменено на выражение warning . correctSpaces . asciiOnly
. Сделаем же это:
logWarn "Province 'Gia Viễn' isn't on the map! "
= (warning
. correctSpaces
. asciiOnly) "Province 'Gia Viễn' isn't on the map! "
= warning
. correctSpaces
. asciiOnly $ "Province 'Gia Viễn' isn't on the map! "
И всё работает! В мире Haskell принято именно так: если что-то может быть упрощено — мы это упрощаем.
Справедливости ради следует заметить, что не все Haskell-разработчики любят избавляться от круглых скобок, некоторые предпочитают использовать именно их. Что ж, это лишь вопрос стиля и привычек.
Если вдруг вы подумали, что оператор композиции уникален и встроен в Haskell — спешу вас разочаровать. Никакой магии, всё предельно просто. Этот стандартный оператор определён так же, как и любая другая функция. Вот его определение:
(.) f g = \x -> f (g x)
Опа! Да тут и вправду нет ничего особенного. Оператор композиции применяется к двум функциям. Стоп, скажете вы, как это? Применяется к функциям? Да, именно так. Ведь мы уже выяснили, что функциями можно оперировать как данными. А раз так, что нам мешает передать функцию в качестве аргумента другой функции? Что нам мешает вернуть функцию из другой функции? Ничего.
Оператор композиции получает на вход две функции, а потом всего лишь даёт нам ЛФ, внутри которой происходит обыкновенный последовательный вызов этих двух функций через скобки. И никакой магии:
(.) f g = \x -> f (g x)
берём эту и эту и возвращаем
функцию функцию ЛФ, внутри
которой
вызываем их
Подставим наши функции:
(.) putStrLn checkLocalhost = \x -> putStrLn (checkLocalhost x)
Вот так и происходит «объединение» двух функций: мы просто возвращаем ЛФ от одного аргумента, внутри которой правоассоциативно вызываем обе функции. А аргументом в данном случае является та самая строка с IP-адресом:
(\x -> putStrLn (checkLocalhost x)) "173.194.22.100" =
putStrLn (checkLocalhost "173.194.22.100"))
Но если я вас ещё не убедил, давайте определим собственный оператор композиции функций! Помните, я говорил вам, что ASCII-символы можно гибко объединять в операторы? Давайте возьмём плюс со стрелками, он чем-то похож на объединение. Пишем:
-- Наш собственный оператор композиции.
(<+>) f g = \x -> f (g x)
...
main :: IO ()
main = putStrLn <+> checkLocalhost $ "173.194.22.100"
Выглядит необычно, но работать будет так, как и ожидается: мы определили собственный оператор <+>
с тем же функционалом, что и стандартный оператор композиции. Поэтому можно написать ещё проще:
(<+>) f g = f . g
Мы говорим: «Пусть оператор <+>
будет эквивалентен стандартному оператору композиции функций.». И так оно и будет. А можно — не поверите — ещё проще:
f <+> g = f . g
И это будет работать! Раз оператор предназначен для инфиксного применения, то мы, определяя его, можно сразу указать его в инфиксной форме:
f <+> g = f . g
пусть
такое
выражение
будет
равно
такому
выражению
Теперь мы видим, что в композиции функций нет ничего сверхъестественного. Эту мысль я подчёркиваю на протяжении всей книги: в Haskell нет никакой магии, он логичен и последователен.