denisshevchenko/ohaskell.guide

View on GitHub
chapters/14-function-composition.md

Summary

Maintainability
Test Coverage
# Композиция функций

Эта глава рассказывает о том, как объединять функции в цепочки, а также о том, как избавиться от круглых скобок.

## Скобкам — бой!

Да, я не люблю круглые скобки. Они делают код визуально избыточным, к тому же нужно следить за симметрией скобок открывающих и закрывающих. Вспомним пример из главы про кортежи:

```haskell
main :: IO ()
main =
  putStrLn (patientEmail ( "63ab89d"
           ^             , "John Smith"
                         , "johnsm@gmail.com"
                         , 59
                         ))
                          ^
```

Со скобками кортежа мы ничего сделать не можем, ведь они являются синтаксической частью кортежа. А вот скобки вокруг применения функции `patientEmail` мне абсолютно не нравятся. К счастью, мы можем избавиться от них. Но прежде чем искоренять скобки, задумаемся вот о чём.

Если применение функции представляет собой выражение, не можем ли мы как-нибудь компоновать их друг с другом? Конечно можем, мы уже делали это много раз, вспомните:

```haskell
main :: IO ()
main = putStrLn (checkLocalhost "173.194.22.100")
```

Здесь компонуются две функции, `putStrLn` и `checkLocalhost`, потому что тип выражения на выходе функции `checkLocalhost` совпадает с типом выражения на входе функции `putStrLn`. Схематично это можно изобразить так:

```haskell
         ┌──────────────┐            ┌────────┐
String ->│checkLocalhost│-> String ->│putStrLn│-> ...
         └──────────────┘            └────────┘

IP-адрес                    сообщение             текст
                            об этом               в нашем
                            IP-адресе             терминале
```

Получается эдакий конвейер: на входе строка с IP-адресом, на выходе — сообщение в нашем терминале. Существует иной способ соединения двух функций воедино.

## Композиция и применение

Взгляните:

```haskell
main :: IO ()
main = putStrLn . checkLocalhost $ "173.194.22.100"
```

Необычно? Перед нами два новых стандартных оператора, избавляющие нас от лишних скобок и делающие наш код проще. Оператор `.` — это оператор композиции функций (англ. function composition), а оператор `$` — это оператор применения (англ. application operator). Эти операторы часто используют совместно друг с другом. И отныне мы будем использовать их чуть ли не в каждой главе.

Оператор композиции объединяет две функции воедино (или компонует их, англ. compose). Когда мы пишем:

```haskell
putStrLn . checkLocalhost
```

происходит маленькая «магия»: две функции объединяются в новую функцию. Вспомним наш конвейер:

```haskell
         ┌──────────────┐            ┌────────┐
String ->│checkLocalhost│-> String ->│putStrLn│-> ...
         └──────────────┘            └────────┘
A                           B                     C
```

Раз нам нужно попасть из точки `A` в точку `C`, нельзя ли сделать это сразу? Можно, и в этом заключается суть композиции: мы берём две функции и объединяем их в третью функцию. Раз `checkLocalhost` приводит нас из точки `A` в точку `B`, а функция `putStrLn` — из точки `B` в `C`, тогда композиция этих двух функций будет представлять собой функцию, приводящую нас сразу из точки `A` в точку `C`:

```haskell
         ┌─────────────────────────┐
String ->│checkLocalhost + putStrLn│-> ...
         └─────────────────────────┘
A                                      C
```

В данном случае знак `+` не относится к конкретному оператору, я лишь показываю факт «объединения» двух функций в третью. Теперь-то нам понятно, почему в типе функции, в качестве разделителя, используется стрелка:

```haskell
checkLocalhost :: String -> String
```

в нашем примере это:

```haskell
checkLocalhost :: A -> B
```

Она показывает наше движение из точки `A` в точку `B`. Поэтому часто говорят о «функции из `A` в `B`». Так, о функции `checkLocalhost` можно сказать как о «функции из `String` в `String`».

А оператор применения работает ещё проще. Без него код был бы таким:

```haskell
main :: IO ()
main =
  (putStrLn . checkLocalhost)  "173.194.22.100"

  объединённая функция         аргумент
```

Но мы ведь хотели избавиться от круглых скобок, а тут они опять. Вот для этого и нужен оператор применения. Его схема проста:

```haskell
FUNCTION  $            ARGUMENT
вот эта   применяется  вот этому
функция   к            аргументу
```

Для нашей объединённой функции это выглядит так:

```haskell
main :: IO ()
main =
  putStrLn . checkLocalhost  $            "173.194.22.100"

  объединённая функция       применяется
                             к            этому аргументу
```

Теперь получился настоящий конвейер: справа в него «заезжает» строка и движется «сквозь» функции, а слева «выезжает» результат:

```haskell
main = putStrLn . checkLocalhost $  "173.194.22.100"

     <-         <-               <- аргумент
```

Чтобы было легче читать композицию, вместо оператора `.` мысленно подставляем фразу &laquo;применяется после&raquo;:

```haskell
putStrLn  .            checkLocalhost

эта       применяется  этой
функция   после        функции
```

То есть композиция правоассоциативна (англ. right-associative): сначала применяется функция справа, а затем &mdash; слева.

Ещё одно замечание про оператор применения функции. Он весьма гибок, и мы можем написать так:

```haskell
main = putStrLn . checkLocalhost $ "173.194.22.100"

       объединённая функция        └─ её аргумент ─┘
```

а можем и так:

```haskell
main = putStrLn $ checkLocalhost "173.194.22.100"

       обычная    └──────── её аргумент ────────┘
       функция
```

Эти две формы, как вы уже поняли, эквивалентны. Я показываю это для того, чтобы вновь и вновь продемонстрировать вам, сколь гибко можно работать с данными и функциями в Haskell.

## Длинные цепочки

Красота композиции в том, что компоновать мы можем сколько угодно функций:

```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: "` в начало сообщения). При запуске этой программы мы увидим:

```bash
WARNING: Province 'Gia Vi?n' isn't on the map!
```

Здесь мы объединили в &laquo;функциональный конвейер&raquo; уже три функции, безо всяких скобок. Вот как это получилось:

```haskell
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 принято именно так: если что-то может быть упрощено &mdash; мы это упрощаем.

Справедливости ради следует заметить, что не все Haskell-разработчики любят избавляться от круглых скобок, некоторые предпочитают использовать именно их. Что ж, это лишь вопрос стиля и привычек.

## Как работает композиция

Если вдруг вы подумали, что оператор композиции уникален и встроен в Haskell &mdash; спешу вас разочаровать. Никакой магии, всё предельно просто. Этот стандартный оператор определён так же, как и любая другая функция. Вот его определение:

```haskell
(.) f g = \x -> f (g x)
```

Опа! Да тут и вправду нет ничего особенного. Оператор композиции применяется к двум функциям. Стоп, скажете вы, как это? Применяется к функциям? Да, именно так. Ведь мы уже выяснили, что функциями можно оперировать как данными. А раз так, что нам мешает передать функцию в качестве аргумента другой функции? Что нам мешает вернуть функцию из другой функции? Ничего.

Оператор композиции получает на вход две функции, а потом всего лишь даёт нам ЛФ, внутри которой происходит обыкновенный последовательный вызов этих двух функций через скобки. И никакой магии:

```haskell
(.)    f        g        =  \x -> f (g x)

берём  эту      и эту       и возвращаем
       функцию  функцию     ЛФ, внутри
                            которой
                            вызываем их
```

Подставим наши функции:

```haskell
(.) putStrLn checkLocalhost = \x -> putStrLn (checkLocalhost x)
```

Вот так и происходит &laquo;объединение&raquo; двух функций: мы просто возвращаем ЛФ от одного аргумента, внутри которой правоассоциативно вызываем обе функции. А аргументом в данном случае является та самая строка с IP-адресом:

```haskell
(\x -> putStrLn (checkLocalhost x)) "173.194.22.100" =

putStrLn (checkLocalhost "173.194.22.100"))
```

Но если я вас ещё не убедил, давайте определим собственный оператор композиции функций! Помните, я говорил вам, что ASCII-символы можно гибко объединять в операторы? Давайте возьмём плюс со стрелками, он чем-то похож на объединение. Пишем:

```haskell
-- Наш собственный оператор композиции.
(<+>) f g = \x -> f (g x)

...

main :: IO ()
main = putStrLn <+> checkLocalhost $ "173.194.22.100"
```

Выглядит необычно, но работать будет так, как и ожидается: мы определили собственный оператор `<+>` с тем же функционалом, что и стандартный оператор композиции. Поэтому можно написать ещё проще:

```haskell
(<+>) f g = f . g
```

Мы говорим: &laquo;Пусть оператор `<+>` будет эквивалентен стандартному оператору композиции функций.&raquo;. И так оно и будет. А можно &mdash; не поверите &mdash; ещё проще:

```haskell
f <+> g = f . g
```

И это будет работать! Раз оператор предназначен для инфиксного применения, то мы, определяя его, можно сразу указать его в инфиксной форме:

```haskell
f <+> g    =      f . g

           пусть

такое
выражение
           будет
           равно
                  такому
                  выражению
```

Теперь мы видим, что в композиции функций нет ничего сверхъестественного. Эту мысль я подчёркиваю на протяжении всей книги: в Haskell нет никакой магии, он логичен и последователен.