Главная / Курсы / О Haskell по-человечески / Композиция функций
# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!