# Глава 13. Композиция функций
Эта глава рассказывает о том, как объединять функции в цепочки, а также о том, как избавиться от круглых скобок.
## Скобкам — бой!
Да, я не люблю круглые скобки. Они делают код визуально избыточным, к тому же нужно следить за симметрией скобок открывающих и закрывающих. Вспомним пример из [главы про кортежи:](/courses/haskell/chapters/haskell_chapter_0110#block-patient-email)
```haskell {.example_for_playground .example_for_playground_001}
main :: IO ()
main =
putStrLn (patientEmail ( "63ab89d"
, "John Smith"
, "johnsm@gmail.com"
, 59
))
```
Со скобками кортежа мы ничего сделать не можем, ведь они являются синтаксической частью кортежа. А вот скобки вокруг применения функции `patientEmail` мне абсолютно не нравятся. К счастью, мы можем избавиться от них. Но прежде чем искоренять скобки, задумаемся вот о чём.
Если применение функции представляет собой выражение, не можем ли мы как-нибудь компоновать их друг с другом? Конечно можем, мы уже делали это много раз, [вспомните:](/courses/haskell/chapters/haskell_chapter_0050#block-check-localhost)
```haskell {.example_for_playground .example_for_playground_002}
main :: IO ()
main = putStrLn (checkLocalhost "173.194.22.100")
```
Здесь компонуются две функции, `putStrLn` и `checkLocalhost`, потому что тип выражения на выходе функции `checkLocalhost` совпадает с типом выражения на входе функции `putStrLn`. Схематично это можно изобразить так:
```
┌──────────────┐ ┌────────┐
String ->│checkLocalhost│-> String ->│putStrLn│-> ...
└──────────────┘ └────────┘
IP-адрес сообщение текст
об этом в нашем
IP-адресе терминале
```
Получается эдакий конвейер: на входе строка с IP-адресом, на выходе — сообщение в нашем терминале. Существует иной способ соединения двух функций воедино.
## Композиция и применение
Взгляните:
```haskell {.example_for_playground .example_for_playground_003}
main :: IO ()
main = putStrLn . checkLocalhost $ "173.194.22.100"
```
Необычно? Перед нами два новых стандартных оператора, избавляющие нас от лишних скобок и делающие наш код проще:
- `.` — оператор композиции функций (англ. function composition).
- `$` — оператор применения (англ. application operator). Его также называют аппликатором функций.
Эти операторы часто используют совместно друг с другом. И отныне мы будем использовать их чуть ли не в каждой главе.
Оператор композиции объединяет две функции воедино (или компонует их, англ. compose). Когда мы пишем:
```haskell
putStrLn . checkLocalhost
```
происходит маленькая «магия»: две функции объединяются в новую функцию. Вспомним наш конвейер:
```
┌──────────────┐ ┌────────┐
String ->│checkLocalhost│-> String ->│putStrLn│-> ...
└──────────────┘ └────────┘
A B C
```
Раз нам нужно попасть из точки `A` в точку `C`, нельзя ли сделать это сразу? Можно, и в этом заключается суть композиции: мы берём две функции и объединяем их в третью функцию. Раз `checkLocalhost` приводит нас из точки `A` в точку `B`, а функция `putStrLn` — из точки `B` в `C`, тогда композиция этих двух функций будет представлять собой функцию, приводящую нас сразу из точки `A` в точку `C`:
```
┌─────────────────────────┐
String ->│checkLocalhost + putStrLn│-> ...
└─────────────────────────┘
A C
```
В данном случае знак `+` не относится к конкретному оператору, я лишь показываю факт «объединения» двух функций в третью. Теперь-то нам понятно, почему в типе функции, в качестве разделителя, используется стрелка:
```haskell
checkLocalhost :: String -> String
```
в нашем примере это:
```haskell
checkLocalhost :: A -> B
```
Она показывает наше движение из точки `A` в точку `B`. Поэтому часто говорят о «функции из `A` в `B`». Так, о функции `checkLocalhost` можно сказать как о «функции из `String` в `String`».
А оператор применения работает ещё проще. Без него код был бы таким:
```haskell {.example_for_playground .example_for_playground_004}
main :: IO ()
main =
(putStrLn . checkLocalhost) "173.194.22.100"
```
Здесь `(putStrLn . checkLocalhost)` — объединенная функция, примененная к аргументу `"173.194.22.100"`. Но мы ведь хотели избавиться от круглых скобок, а тут они опять. Вот для этого и нужен оператор применения. Его схема проста:
```
FUNCTION $ ARGUMENT
вот эта применяется вот этому
функция к аргументу
```
Есть идеи, за счет чего оператор `$` позволяет избавиться от скобок? Дело в том, что обычное применение функции (функция и через пробел аргументы) имеет высший приоритет, а оператор `$` имеет минимальный приоритет. Визуализировать работу оператора применения можно так: представьте, что на месте знака доллара `$` открывается круглая скобка, а в конце определения функции круглая скобка закрывается.
Для нашей объединённой функции это выглядит так: объединенная функция `putStrLn . checkLocalhost` с помощью `$` применяется к аргументу `"173.194.22.100"`:
```haskell
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.
Перепишите тело функций `f` и `main`: избавьтесь от круглых скобок. {.task_text}
```haskell {.task_source #haskell_chapter_0130_task_0010}
module Main where
f :: Int -> Int -> Int
f x y = succ (max x y)
main :: IO ()
main = print (f 5 20)
```
Воспользуйтесь оператором применения. {.task_hint}
```haskell {.task_answer}
module Main where
f :: Int -> Int -> Int
f x y = succ $ max x y
main :: IO ()
main = print $ f 5 20
```
Перепишите тело функции `main`: избавьтесь от круглых скобок. {.task_text}
```haskell {.task_source #haskell_chapter_0130_task_0020}
module Main where
square :: Double -> Double
square x = x*x
main :: IO ()
main = putStrLn (show (square 5.1))
```
Воспользуйтесь операторами композиции и применения. {.task_hint}
```haskell {.task_answer}
module Main where
square :: Double -> Double
square x = x*x
main :: IO ()
main = putStrLn . show . square $ 5.1
```
## Длинные цепочки
Красота композиции в том, что компоновать мы можем сколько угодно функций:
```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` готовит строку к выводу в нелокализованном терминале (да, в наши дни такие всё ещё имеются), функция `correctSpaces` убирает дублирующиеся пробелы, а функция `warning` делает строку предупреждением (например, добавляет строку `"WARNING: "` в начало сообщения). При запуске этой программы мы увидим:
```bash
WARNING: Province 'Gia Vi?n' isn't on the map!
```
Здесь мы объединили в «функциональный конвейер» уже три функции, безо всяких скобок. Вот как это получилось:
```
warning . correctSpaces . asciiOnly $ rawMessage
^
└── первая композиция ──┘
^
└────── вторая композиция ────────┘
аргумент
```
Первая композиция объединяет две простые функции, `correctSpaces` и `asciiOnly`. Вторая объединяет тоже две функции, простую `warning` и объединённую, являющуюся результатом первой композиции.
Более того, определение функции `logWarn` можно сделать ещё более простым:
```haskell
logWarn :: String -> String
logWarn = warning . correctSpaces . asciiOnly
```
Погодите, но где же имя аргумента? А его больше нет, оно нам не нужно. Ведь мы знаем, что применение функции можно легко заменить внутренним выражением функции. А раз так, выражение `logWarn` может быть заменено на выражение `warning . correctSpaces . asciiOnly`. Сделаем же это:
```haskell
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 принято именно так: если что-то может быть упрощено — мы это упрощаем.
Перепишите тело функции `main` с использованием операторов композиции и применения. {.task_text}
Если забыли, [что это](/courses/haskell/chapters/haskell_chapter_0090##block-cons) за оператор `:` и [что делают](/courses/haskell/chapters/haskell_chapter_0090##block-useful-functions) функции `product` и `tail`, то разговор об этом велся в [главе про списки.](/courses/haskell/chapters/haskell_chapter_0090/) {.task_text}
```haskell {.task_source #haskell_chapter_0130_task_0030}
module Main where
main :: IO ()
main = print (product (tail (8:10:3:3:2:[])))
```
Объедините в «функциональный конвейер» три функции. Воспользуйтесь оператором применения для передачи в этот конвейер аргумента. {.task_hint}
```haskell {.task_answer}
module Main where
main :: IO ()
main = print . product . tail $ (8:10:3:3:2:[])
```
Справедливости ради следует заметить, что не все Haskell-разработчики любят избавляться от круглых скобок, некоторые предпочитают использовать именно их. Что ж, это лишь вопрос стиля и привычек.
## Как работает композиция
Если вдруг вы подумали, что оператор композиции уникален и встроен в Haskell — спешу вас разочаровать. Никакой магии, всё предельно просто. Этот стандартный оператор определён так же, как и любая другая функция. Вот его определение:
```haskell
(.) f g = \x -> f (g x)
```
Опа! Да тут и вправду нет ничего особенного. Оператор композиции применяется к двум функциям. Стоп, скажете вы, как это? Применяется к функциям? Да, именно так.
Ведь мы уже выяснили, что функциями можно оперировать как данными. А раз так, что нам мешает передать функцию в качестве аргумента другой функции? Что нам мешает вернуть функцию из другой функции? Ничего.
Оператор композиции получает на вход две функции, а потом всего лишь даёт нам ЛФ, внутри которой происходит обыкновенный последовательный вызов этих двух функций через скобки. И никакой магии:
```
(.) f g = \x -> f (g x)
берём эту и эту и возвращаем
функцию функцию ЛФ, внутри
которой
вызываем их
```
Подставим наши функции:
```haskell
(.) putStrLn checkLocalhost = \x -> putStrLn (checkLocalhost x)
```
Вот так и происходит «объединение» двух функций: мы просто возвращаем ЛФ от одного аргумента, внутри которой правоассоциативно вызываем обе функции. По принципу правоассоциативности начала применяется функция справа (то есть `checkLocalhost`), а затем — слева (то есть `putStrLn`). А аргументом в данном случае является та самая строка с IP-адресом:
```haskell
(\x -> putStrLn (checkLocalhost x)) "173.194.22.100" =
putStrLn (checkLocalhost "173.194.22.100"))
```
Но если я вас ещё не убедил, давайте определим собственный оператор композиции функций! Помните, я говорил вам, что ASCII-символы можно гибко объединять в операторы? {.task_text}
Возьмите плюс со стрелками, он чем-то похож на объединение: `<+>`. Определите через него оператор композиции функций. Затем используйте этот оператор в теле `main` совместно с оператором применения. {.task_text}
Обратите внимание, что в коде нет определения, но уже есть объявление оператора `<+>`. О том, что означают маленькие буквы `a`, `b` и `c` в этом объявлении, вы узнаете уже в следующей главе! {.task_text}
```haskell {.task_source #haskell_chapter_0130_task_0040}
module Main where
(<+>) :: (a -> b) -> (c -> a) -> c -> b
-- Your code here
checkLocalhost :: String -> String
checkLocalhost ip =
if ip == "127.0.0.1" || ip == "0.0.0.0"
then "It's a localhost"
else "No, it's not a localhost"
main :: IO ()
main = putStrLn (checkLocalhost "173.194.22.100") -- And here
```
Так будет выглядеть использование оператора: `putStrLn <+> checkLocalhost $ "173.194.22.100"`. {.task_hint}
```haskell {.task_answer}
module Main where
(<+>) :: (a -> b) -> (c -> a) -> c -> b
(<+>) f g = \x -> f (g x)
checkLocalhost :: String -> String
checkLocalhost ip =
if ip == "127.0.0.1" || ip == "0.0.0.0"
then "It's a localhost"
else "No, it's not a localhost"
main :: IO ()
main = putStrLn <+> checkLocalhost $ "173.194.22.100"
```
Оператор `<+>`, определенный вами в этой задаче, выглядит необычно. Но работает так, как и ожидается: он обладает ровно тем же функционалом, что и стандартный оператор композиции. Поэтому можно написать ещё проще:
```haskell
(<+>) f g = f . g
```
Мы говорим: «Пусть оператор `<+>` будет эквивалентен стандартному оператору композиции функций». И так оно и будет. А можно — не поверите — ещё проще:
```haskell
f <+> g = f . g
```
И это будет работать! Раз оператор предназначен для инфиксного применения, то мы, определяя его, можем сразу указать его в инфиксной форме.
Кстати, оператор применения определяется еще проще:
```haskell
($) :: (a –> b) –> a –> b
f $ x = f x
```
Перед вами несколько вариантов композиции функций. Перечислите через пробел по номерам, какие из них являются корректными. {.task_text}
```haskell
putStrLn . show . square $ 8 -- 1
putStrLn $ show . square 8 -- 2
putStrLn . show $ square $ 8 -- 3
putStrLn show . square $ 8 -- 4
putStrLn $ show $ square $ 8 -- 5
```
```consoleoutput {.task_source #haskell_chapter_0130_task_0050}
```
Ошибки допущены во 2-м и 4-м вариантах. {.task_hint}
```haskell {.task_answer}
1 3 5
```
Теперь мы видим, что в композиции функций нет ничего сверхъестественного. Эту мысль я подчёркиваю на протяжении всего курса: в Haskell нет никакой магии, он логичен и последователен.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!