Сила композиции

rl6vo4wbgzp-jjacg0fdoxgr7ko.jpeg

Функциональное программирование может отпугивать сложностью и непрактичностью: «Я далек от всех этих монад, пишу на обычном C#, в докладе про функциональщину ничего не пойму. А если даже напрягусь и пойму, где мне потом это применять?»

Но когда объясняет Скотт Влашин, все совершенно не так: его доклад о композиции с конференции DotNext 2019 Moscow — пример того, как можно доносить функциональные идеи простыми словами. Он за час перешел от бананов к монадам так, что второе кажется немногим сложнее первого. А в конце объяснил, почему осмыслить композицию полезно даже тем, кто не собирается покидать мир ООП. Примеры кода в докладе как на F#, так и на C#.

Уже завтра начнется новый DotNext, где я помогу Скотту выступить с другим докладом, а пока что публикую перевод его выступления про композицию. Далее повествование будет от лица Скотта.



Оглавление


  1. Философия композиции
  2. Идеи функционального программирования
    — Функции и их композиция
    — Типы и их композиция
  3. Композиция на практике
    — Римские цифры
    — Нетипичный FizzBuzz
    — Охохонюшки, монады!
    — Веб-сервис
  4. Итоги

Философия композиции

Чтобы понять композицию, необходимо обладать некоторым опытом:


  • Вы должны были быть ребенком.
  • У вас должен быть опыт игры в Lego.
  • И опыт с игрушечной железной дорогой.

Поскольку ребенком был каждый, с первым пунктом сразу порядок. Теперь давайте поговорим про Lego.


Философия Lego

rtzere66_1ihobr76n-jxkk6pva.jpeg


  1. Все детали предназначены для соединения с другими деталями. На них есть маленькие «кнопочки», позволяющие их соединять.
  2. Детали можно переиспользовать в разных контекстах. Из одних и тех же деталей можно построить совершенно разные вещи. И, что очень важно, каждая деталь самодостаточна: чтобы она выполняла свою функцию, не требуется электричество или какие-то другие детали.
  3. Если соединить две детали Lego, то вы получите «новую деталь Lego», которую можно снова соединить с другой деталью, и так до бесконечности. Если собирать детали достаточно долго, можно в итоге получить очень большие фигуры.

Как, например, эта:

b-obkad0uiy93yqbyck3tntxgtu.jpeg


Спойлер

И даже на ней все еще есть кнопочки, и мы можем продолжить строить и строить.

Это я и называю силой композиции.


Философия игрушечной железной дороги

Здесь все то же самое:


  1. Все детали предназначены для соединения с другими.
  2. Каждую можно использовать для разных дорог.
  3. Если соединить две детали, то мы получим деталь побольше, которую опять можем использовать и строить дороги все длиннее и длиннее.

joqgmenwpjvkgmq5xmmuqcvnopu.jpeg

Eсли вы понимаете, как работают Lego и игрушечная железная дорога, то, в принципе, вы понимаете все про композицию, можно расходиться!



Идеи функционального программирования

yt_8w2rbh4imybtxyus81e_otxg.jpeg


1. Функция — это сущность

Функции — это не методы, это сущности. Функцию можно рассматривать как кусочек железной дороги с туннелем. Я называю его «туннель трансформации», потому что заезжает в него одна сущность, а выезжает другая. Вот функция, преобразующая яблоки в бананы:

ymb87xncpr52bhqnelbc_aql9py.jpeg

Важно понять, что это автономная единица, а не метод, который привязан к классу. «Автономная» можно понимать как «повторно-используемая». Это сущность, которую можно использовать как и другие сущности — int, String или DateTime.

Функция может быть использована как входное значение для другой функции или быть результатом ее выполнения: везде, где вы можете использовать int или String, можете использовать и функцию.

Рассмотрим примеры четырёх ситуаций.

Функция как результат выполнения функции:

5m8sck-jiaype-dozcqoevezmdm.jpeg

Функция как входное значение:

cjzee-ksxfnii5dms47_dm9l8i8.jpeg

Функция как параметр:

nw3ze0-htmyzatiyu11b7pmxch0.jpeg

Функция как входное значение и как результат выполнения:

8twucoktzciqcq3dbkhmaqpmbky.jpeg

Это и есть суть функционального программирования. Звучит довольно прямолинейно, но, конечно, на практике всё может быть непросто. У вас может быть функция, которая возвращает функцию, которая порождает функции, которые используют функции как параметр. Но идея за всем этим стоит всё та же.


2. Композиция позволяет «строить» более крупные функции

Давайте попробуем создать одной функции из двух больших. Как нам их соединить?

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

q7yc7-deyaui9krb6y5vl_m7s1o.jpeg

И тогда мы получим новую функцию, которая преобразует яблоки в вишни. И что важно, вы не видите, что на самом деле новая функция — это композиция двух других функций:

e1vd8zcsgukkboynyp16manjegm.jpeg

Но куда же делся банан?

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

И, конечно, вы можете взять эту функцию и продолжить составлять с ее помощью все новые и новые функции.

Давайте посмотрим, как это можно сделать на C# и F#. Возьмем три метода на С#:

int add1(int x) => x + 1;
int times2(int x) => x * 2;
int square(int x) => x * x;

А затем к 5 прибавим 1 (add1), умножим на 2 (times2) и возведем все в квадрат (square).

add1(5);
times2(add1(5));
square(times2(add1(5)));

При обычном подходе мы получаем множество скобочек. Чтобы разобраться, что к чему, придется найти самую вложенную функцию посередине и затем «прорываться наружу» через все эти скобочки, просматривая функцию за функцией. И порой это сложно.

Функциональный подход заключается в создании «пайплайнов» или конвейеров. Если вы знаете о UNIX pipes, то это то же самое.

Мы берем 5 и «скармливаем» ее функции add1. Потом берем результат и передаем в функцию times2. Затем, то, что получилось, передаем в square и получаем 144.

vhbznyo0k-1nkmrqqyxydkh1a7o.jpeg

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

Как сделать такой пайплайн на F#? Там у нас есть специальный оператор |> (аналог оператора | в UNIX), который означает «взять результат и скормить его дальше»:

-stwlove17isngsm_lg_8kdcmri.jpeg

В F# этот оператор встречаешь на каждом шагу. В C# такого нет, однако мы можем написать extension method, работающий с любым объектом:

pppwvddswrlzqsbzpwgxpxmtdbg.jpeg

Может выглядеть странновато, но смотрите на это как на UNIX-пайплайны.

Теперь поговорим о составлении больших систем из функций.

Допустим, у вас есть низкоуровневая операция. Например, функция ToUpper (), которая переводит строку в верхний регистр.

tjruee_z6_wei0e4npxauy3kwkm.jpeg

И вы берете несколько низкоуровневых операций и соединяете их в сервис. Например, сервис, валидирующий адрес. Он принимает адрес, а выдает результат валидации.

kbntjpfxjgpan7jwkwiebja9qkm.jpeg

Я называю это словом «сервис», потому что мне уже за сорок. Если вы младше, можете говорить «микросервис».

А дальше возьмем несколько сервисов, соединим вместе, и появляется уже определенный пользовательский сценарий. Например, обновление данных профиля.

uqrzkdj3grgmrqj8g-r-lm77qbg.png

А теперь поговорим о целом веб-приложении. Если вдуматься, то веб-приложение — это функция. Её входное значение — это HttpRequest, а результат — HttpResponse.

gvkjc1wwcvspzyglrs0rbfuouuc.png

И внутри этой большой функции вам просто нужно решить, какую из функций поменьше (какой пользовательский сценарий) вам надо вызвать.

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

Вот что мы увидим, заглянув внутрь веб-приложения. Есть функции поменьше, и, конечно, есть ветвление, это не просто единый пайплайн с начала до конца. Но интересно, что даже в сложных приложениях данные движутся в одном направлении — без циклов и спутанных клубков. В функциональной парадигме данные почти всегда однонаправленные. И это делает всё лёгким для понимания.


3. Типы — это не классы

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

Давайте посмотрим на функцию. У нее есть множество значений, которые она может принять, и множество значений, которые эта функция может вернуть:

z9jjb9erhlfbdrzhonufigz81bw.jpeg

И тип — это просто название для таких множеств.

Напрмер, мы можем сказать, что функция примет любое целое число, так что мы назовем этот тип «Integer», а вернуть может какую угодно строку, и этот тип назовем «String». Выглядит как класс, но это просто множество, там нет методов.

Из-за того что тип — это всего лишь множество, там может быть что угодно, хоть все люди в мире, хоть все существующие фрукты. И в том числе типом может быть функция:

mllzshmrddk3cf92ihn0bg2n6ym.jpeg


4. Композиция позволяет «строить» более крупные типы

Из типов тоже можно составлять композиции. Раз это просто множества, то все, что мы можем сделать с множествами, мы можем делать и с типами. Можем брать объединение двух типов, пересечение, декартово произведение, потому что у типов не прописано поведение, у них нет никаких методов.

В функциональном программировании это называется алгебраической системой типов, но я называю это композиционной системой типов, потому что суть тут в композиции.

Мы можем составлять большие типы из маленьких двумя способами:


  1. С помощью логического «И».
  2. С помощью логического «ИЛИ».

Давайте рассмотрим каждый способ в отдельности.

Композиция с помощью «И». Допустим, мы хотим создать фруктовый салат. Это яблоко И банан И вишня.

y0dlsduo6hndwnlxhk3mucowccc.jpeg

И как же нам создать это в виде типа?

В С# для этого используются struct и enum:

pywugrb021vvblwdofqpdl5xh1c.jpeg

Вместо struct можно использовать классы, но у struct нет никаких методов, поэтому это больше походит на типы.

В F# все похоже:

cb8xpbi61ioplto9twapllxgsd8.jpeg

Заметьте, что объявление типа идет после названия переменной. В этом плане F# больше похож на JavaScript.

В мире F# мы называем полученный тип «record type».


В C# 9 тоже появились «записи».

В принципе, с этим вы уже знакомы по C#. А вот что вы не так хорошо знаете — это композиция с помощью «ИЛИ».

Допустим, я хочу перекусить, и для меня перекус — это яблоко ИЛИ банан ИЛИ вишня.

ybwf_l7znlipecnedst1qllavdm.jpeg

В C# нет подходящих инструментов для подобного, поэтому давайте посмотрим на F# код:


В C# 9 есть библиотеки, эмулирующие discriminated unions.

cfoahftn_gh2m-_rb_j-dahbnlw.png

Для «ИЛИ» используется вертикальная черта.

В F# такие типы называются discriminated unions, в других языках их называют sum types, а я называю их «choice» types, потому что мы выбираем между этими вариантами.

Эти типы можно представлять себе как enum с дополнительной информацией для каждого выбора, и в C# такого сделать нельзя.

Как это может пригодиться? Давайте представим, что мы делаем платежную систему, которая принимает наличные, Paypal и банковские карты:


  • Для получения наличных нам не нужна никакая дополнительная информация.
  • Если оплата идет через Paypal, то нам нужен email.
  • А если через карту, то нужен ее тип и номер.

То есть для каждого случая есть некоторая дополнительная информация. И как же нам это реализовать?

Если мы пишем в объектно-ориентированном подходе, то, скорее всего, мы создадим интерфейс IPaymentMethod, и после напишем 3 класса, которые реализуют этот интерфейс. И для каждой реализации нам нужны будут разные данные.

co3mnzocrkr_pblu2rgqogh-aac.png

Это стандартный объектно-ориентированный подход к решению данной задачи. А теперь давайте посмотрим, как делать это с помощью композиции. В F# мы строим большие типы из маленьких.

Начнем с примитивов. Email и номер карты — это просто строки.

wfh-ac-fyg6ct1rb0okp2vbiyfm.png

Затем определим тип карты. Это Visa ИЛИ MasterCard, то есть выбор, choice type.

jv0opr-jtdhb_zktrcm8k5li27s.png

Далее добавим информацию о карте. Это тип карты И номер карты, то есть record type.

dyceuahyrzj_01vxmxhxun1xuxw.png

Теперь мы можем написать наш тип оплаты. Это наличные ИЛИ PayPal ИЛИ карта. В каждом случае своя соответствующая информация — в первом случае не требуется ничего, во втором адрес, в третьем данные карты.

zv-tcimlctbjlqfxsvjfcskc_kw.png

Вот так мы бы написали систему оплаты на F#. И что самое классное, мы можем и не останавливаться, совсем как в Lego. Давайте продолжим, добавим размер платежа и валюту! Размер — просто число, валюта — выбор между евро, долларом и рублем.

716ookte-rxq8vfzgow6zpmj2fm.png

Теперь мы можем сделать тип «Платеж»: это размер платежа И валюта И тип оплаты.

e7i-oov8ysifn34sitow4xdqshe.png

Так вместо привычного наследования мы тут растем «снизу вверх» и собираем большие типы из маленьких. Поначалу это может быть непривычно, но когда осваиваешься, очень здорово.

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

jl_1zyqdbrm18gpz5aewb96yujk.jpeg

Думаю, вполне понятно, что это все как-то связано с карточными играми. Здесь есть «карта» (Card), «колода» (Deck) и «карты на руке» (Hand).

Но мы не ограничиваемся только существительными, мы также можем описать и глаголы. Например, если вы хотите взять карту (PickupCard), то у вас в качестве входных данных есть Hand и Card, а в результате вы получаете новый набор карт на руке Hand.

То есть мы можем смоделировать как вещи, так и действия.

И если вы работаете с людьми вроде бизнес-аналитиков, которые не разбираются в программировании, то такой код — это отличный способ взаимодействия с ними, потому что они могут читать его и указать вам на ваши ошибки.

Также такой подход облегчает жизнь новым разработчикам. Допустим, вы пришли в новую команду, и вам нужно узнать, сколько методов оплаты поддерживает приложение. Сможете ли вы понять это самостоятельно по этому коду, учитывая, что вы не читали документацию?

h6zjnslfzjzmsqb9v4lyqtzspim.jpeg

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

На этом заканчиваем с теорией. Всё тут не перескажешь, но про Domain-Driven Design я написал целую книгу «Domain Modeling Made Functional», а на сайте об этом сделал специальный раздел.



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


Композиция с помощью пайплайна (на примере римских цифр)

Как перевести арабские числа в римские?

j18djwmfhah56tjircopwp6vfri.png

Есть разные способы решения. Мой основан на том, что римские цифры исходно отталкивались от зарубок на стенах. Когда набиралось четыре, дальше пятой их перечёркивали — думаю, оттуда и пошло «V».

Алгоритм будет следующим:


  1. Сначала мы n раз пишем «I», где n — это наше число на входе.
  2. Далее мы заменяем:
    • Каждые 5 «I» на «V»
    • 2 «V» на «X»
    • 5 «X» на «L»
    • И 2 «L» на «C»
    • И так далее

По сути, это пайплайн (или конвейер). Мы сначала заменяем все «I» на «V», после берем результат и передаем следующей функции, которая заменяет «V» на «X», и так далее. Мы как бы «пропускаем» данные через эти функции.

mjrn6gkysn2jaqichz8dhh8nij8.png

Давайте посмотрим, как мы можем сделать это на C#:

piaqrgtisuuj7kccd2lssd4s4po.png

Это код на C# в функциональном стиле. Мы определили несколько маленьких функций, которые потом «пайпнули» с помощью extension-метода Pipe для object.

А теперь на F#. По сути, то же самое,

tearp-txpidqt-xgwvbntjq9oco.png

Как и в C#, у нас есть несколько маленьких функций, которые мы соединяем вместе, но уже с помощью pipe-оператора |>.

Что ж, это были примеры соединения функций (piping), но не всегда всё легко.

Пока что мы имели дело с функциями, у которых одно входное значение и один результат. И при соединении все проходило гладко, они идеально подходили друг другу. Легко!

fkk7h7tg4imdm5uskq3hjabe_ni.png

Но в реальном мире всё сложнее.

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

-szby4pxn0dtiypmmxeh5lvq2k0.png

Как же соединить эти две функции?


Композиция с помощью каррирования (на примере римских цифр)

Рассмотрим еще раз задачу о римских числах.

Если посмотреть на функцию String.Replace, она принимает на вход три вещи: исходную строку (inputString), что заменить (oldValue) и на что заменить (newValue).

zdba1gaxwgxtof03nm2zup7if6m.png

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

tqpeddkpbckhc0r9ihcp_na3kqo.png

23dy1aoj0lamxfueow6dftx0zio.jpeg И вы, наверное, уже подумали, что композиция работает только с функциями, которые принимают одно значение, и это очень плохо, потому что мало какая функция принимает одно значение.

Однако есть небольшая хитрость! Любую функцию можно превратить в функцию с одним входным значением. Для этого используют каррирование. Этот метод назван в честь американского математика и логика Хаскелла Карри (Haskell Curry), так что здесь нет ничего общего с индийской кухней.

У нас есть функция с двумя входными значениями. Чтобы превратить ее в функцию с одним входным значением, мы создадим каррированную версию этой функции, которая принимает одно значение, а возвращает новую функцию, которая принимает еще одно значение.

Помните, я говорил, что раз функции — это сущности, то их можно возвращать?

x3zt8vukrwld-8gxaotdz0ow0dc.png

Теперь у нас есть функция, которую мы можем использовать для композиции!
Это и есть каррирование.

Вернемся к Replace. Изначально это функция с тремя значениями, что для нас нехорошо.

poewpzhpp2f-ld8u-t0mjvfgjss.png

Перепишем ее с помощью каррирования. Я хочу специальную функцию с двумя входными значениями, результатом которой является функция с одним параметром. Вот как это выглядит, если использовать C# в функциональном стиле:

fcbts-1cs_f5ootuv_awyh668qy.jpeg

Это функция с двумя входными параметрами, результат которой — лямбда, которая принимает string и возвращает string.

От всего этого с непривычки может болеть голова, выглядит странно, зачем вообще такое писать? При таком подходе для решения задачи с римскими числами нам не надо писать множество маленьких функций, достаточно одной. Выглядит сложновато, но стоит вникнуть — и все станет очень просто.

fbsquvg5lsndprenzpoa6mrlpuw.png

И теперь при выстраивании пайплайна мы используем одну и ту же функцию, просто с разными значениями.

На C# это все смотрится немного некрасиво, так что давайте посмотрим на F#.

zdnnk7juzdj1j1agcsvbsqnfrie.jpeg

В F# вам не нужны вспомогательные функции, потому что там мы получаем каррирование «из коробки».

Мы просто можем вызвать Replace с двумя параметрами. Но как так, ведь там требуются три?


Частичное применение (Partial application)

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

no2d02gxy8xqpfladv1wcuiwdgm.png

Например, у нас есть две функции, add и multiply. У каждой из них по два параметра, и обычно нам надо передать все параметры сразу. Но в F# вы можете передать один.

aapigsoh_xnq27_yzpupkktjzjs.png

Здесь мы берем число 5 и «скармливаем» его в add, после берем результат и передаем в multiply. Заметьте, что add и multiply принимают только один параметр, а второй приходит из pipe-оператора.

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

Поэтому в F# между параметрами стоят пробелы. Обратите внимание: нет никаких скобок и запятых. И можно обрезать некоторые параметры, сделав новую функцию.

И вот как как выглядит replace с частичным применением. Мы просто передаем два параметра и получаем новую функцию, куда оставшиеся значения приходят из pipe-оператора.

8w5guudhb1vc-clllurfqnwvhm8.png

У вас уйдет какое-то время, чтобы вникнуть во все это, потому что это сильно отличается от объекто-ориентированного подхода.


В JavaScript частичное применение доступно с помощью функции bind.

Композиции расширяемы

Композиции также хороши тем, что они расширяемы. Если мы посмотрим на наш код для решения задачи о римских числах, можно заметить, что мы не учли одной детали. При написании римских чисел мы обычно не пишем «IIII», вместо этого пишем «IV», а вместо «VIIII» пишут «IX», и наша программа этого не учитывает. Однако при композиции мы просто можем добавить это все в конец:

ytxv337csyuu0gzohp3um3s2vzg.jpeg

При этом мы не трогаем наш старый код. И это очень хорошо, ведь последнее, что хочется делать — это изменять уже написанные куски программы, можно сломать там что-то. Можно добавлять код, удалять — еще лучше, а вот менять хуже. И при композиции можно добавить код наверх, вниз или даже посередине, но важно, что вы не изменяете код, поэтому, скорее всего, ничего не сломается.

Что ж, мы разобрались, как соединить функции, у которых множество входных значений. Ответ — каррирование и частичное применение.


Композиция с помощью Bind (на примере FizzBuzz)

Вообще говоря, почти все задачи в функциональном программировании — о том, как правильно объединить функции. И паттерны в функциональном программировании нацелены на то, чтоб соединять такие, которые изначально «не подходят» друг другу.

Рассмотрим случай, когда функция возвращает два значения, и нам надо соединить ее с функцией, принимающей одно значение:

zuw0cw8agipmtf6b_vpz-sct3qq.png

Наверное, все знают задачу FizzBuzz. Вы выводите числа от 1 до 100, но:


  • вместо чисел, кратных трем, вы пишете «Fizz»
  • вместо чисел, кратных пяти — «Buzz»
  • а если число кратно и трем, и пяти, то «FizzBuzz»

Вот как эта задача решается на F#:

9hxvq5qsdkafmfxmvoiijjaolwm.png

На C# будет то же самое. Просто проверяем каждое число в цикле с помощью if else.

Это очень простая реализация FizzBuzz, и если вас попросили решить эту задачу на собеседовании, то лучше написать ее именно так. Но мы решим ее иначе, потому что эта реализация очень легкая, а мы — функциональные программисты и хотим сделать эту задачу настолько сложной, насколько это вообще возможно. Отмечу, что идея взята из блог-поста, призывающего не слишком усложнять FizzBuzz во время реальных собеседований.

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

Сделаем такой пайплайн, соединив несколько маленьких функций:

tfq3a9elaki_ftotcgotwbpwule.png

Сначала мы проверим на кратность 15, потом на кратность 3 и т.д. И если мы захотим что-то поменять, сможем просто добавить функции в наш пайплайн.

В отличие от задачи о римских числах, когда мы разбираем каждое число, есть два возможных варианта: в качестве результата может быть либо то же самое число, либо строка («Fizz», «Buzz», «FizzBuzz»).

umaap358xsjp0fuaqrxvraye9ma.png

Если функция вернула строку, будем говорить, что вернулось обработанное значение, иначе — необработанное значение.

Если представить, что наша функция — это часть железной дороги, то это была бы развилка, где первая ветка (зеленая) — это необработанное значение, а вторая (красная) — обработанное.

4ho12h319vkejbqmnod99vhiywi.png

Заметьте, что результатом функции может быть обработанное ИЛИ необработанное значение. Слово «или» здесь ключевое.

Мы уже знаем, что в F# за «ИЛИ» отвечают choice types. Сделаем и тут такой тип, который может включать как int, так и String.

uolt0pbgmhwfwftrzwxoshdfyta.png

Теперь опишем функцию handle, обрабатывающую каждый случай.

400cwdeufgeig54zvucd9j9hfnq.png

Эта функция проверяет, делится ли число n на число divisor без остатка, и, если делится, то возвращает обработанную строку label, иначе возвращает необработанное число n. Обратите внимание, что функция принимает три входных значения.

По сути это логика, которая была в легкой версии FizzBuzz, вынесенная в отдельную функцию. Посмотрим, как ведет себя эта функция на реальных примерах:

uqglxfvoywm4_2oueb2bpfei-ru.png

Сначала мы проверяем, делится ли 12 на 3, и если делится, возвращаем обработанную строку «Fizz», поэтому в первом вызове мы получили обработанный «Fizz».

Потом в эту же функцию передаем 10, и получаем необработанную 10, потому что 10 не делится на 3.

И, наконец, 10 проверяем на кратность 5 и получаем обработанный «Buzz».

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

Теперь сделаем первую попытку реализовать весь FizzBuzz с помощью композиции:

xomexfdb9bwf841mix5cngwrrdw.png

Сначала мы проверяем, делится ли n на 15, и в качестве результата получим FizzBuzzResult, который потом мы проверяем с помощью match.
Если вернулось обработанное значение, то функция возвращает итоговый результат, если необработанное — проверяем, делится ли n на 3, и повторяем все то же самое. И только если наше n не делится на 5, то возвращается само число n.

На самом деле так писать не надо, это очень плохой код. Но как нам сделать его лучше?

Здесь можно заметить закономерность. На каждой проверке мы встречаем одну и ту же инструкцию: «если результат — необработанное число, сделай что-то».

По сути, она представляет собой это:

ij_wpgg9yo4egbmkycg5x1mn58k.png

Если обработано, то верни значение, если не обработано, то сделай что-то.

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

lj8pfgi_bnvsgp6l9noxxohquog.png

Мы напишем небольшую вспомогательную функцию.

Она говорит: Если result — обработанное значение, то мы просто «проезжаем до конца дороги», но если result — необработанное значение, то нам надо сделать что-то.

Но что именно сделать? Мы сами ещё не знаем. Поэтому вынесем это в отдельную функцию и будем принимать ее в качестве параметра:

hnuxcy0ho3w5rbjfsmuvprlgvpk.png

В этой функции то, что понадобится делать дальше, если result окажется необработанным. И причина, по которой она называется «f», заключается в том, что мы не знаем что это за функция, это может быть что угодно! Можно было назвать её «следующий участок железной дороги».

И с помощью этого можно изменить нашу оригинальную реализацию. Можно сказать: попробуй это, если результат остался необработанным — переходи ко второму шагу, если и после него необработанным — к третьему, и затем последний шаг.

u66i_qmgsjnmyrwp7iiss4tpohk.png

Мы как бы едем по железной дороге и каждую развилку спрашиваем себя: «Мы до сих пор на зеленой (необработанной) ветке?», если да, то продолжаем ехать, если нет, то «выезжаем» с результатом. А lastStep — это место, где дороги сходятся вместе и возвращается результат:

vsomde2dm26aysjpuma0zqy1h8c.png

Теперь мы сделали все пайплайном, и благодаря композиции все легко расширять. Можно добавлять новые проверки, не трогая старый код — достаточно будет просто добавлять функции. Кроме «fizz» и «buzz», легко добавить что угодно и быть уверенным, что оно заработает:

90j9uflbyalsnqot4nvta7eqkt0.png

Это применимо и в той типичной ситуации, когда мы запускаем Task, если эта задача завершилась, то запускаем следующую, а в противном случае просто «проходим в конец». Ну, promise, future, async, вот это все.

esf0kadtvpftmutydoyuytbzyly.png

Вы часто видите код, где начинается Task, после завершения запускается следующий, потом следующий, и так далее. В итоге вы получаете глубоко вложенные функции, и чем больше функций, тем хуже это выглядит и тем сложнее это читается.

yycm2_jxocuggxc8qmcjxm2m0_g.png

Заметьте, здесь тот же паттерн, что и в FizzBuzz: «когда Task завершится, сделай что-то». Поэтому, чтобы исправить ситуацию, мы сделаем то же самое: напишем вспомогательную функцию:

0_kheymtuyvyorz8jbi7o424vg0.png

Когда task завершится, сделай что-то — и мы не знаем, что конкретно, поэтому принимаем для этого функцию f.

Теперь мы можем переписать наш код с использованием нашей новой функции:

jsgyln8xqxctdx0rs3d8txttvtq.png

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

И это подводит нас к следующей теме:


МОНАДЫ!

xlehlsgi8olwnbsi7achc4rm1be.png

Все очень боятся монад, но сегодня мы не будем учить монады, наша цель просто перестать бояться их, чтобы не впадать в панику каждый раз, когда вы слышите это слово.

Что же такое монада?

Монада — это просто общее решение вышеописанных проблем. Если конкретнее, существует понятие Bind.

Очень часто вы сталкиваетесь ситуацией, когда у вас есть функции с «ветвями», и вы хотите их соединить. Начинаете вы с такого:

8sbfdv4hrljliw3ns4bmjlwu_ee.png

А в итоге у вас получается это:

q0il9lwvy0t4ma6ocrrxgrng9ri.png

Я называю такой подход «Railway Oriented Programming» и посвятил ему отдельную страницу на сайте (перевод доступен на Хабре). Это особенно применимо для обработки ошибок, но не только для них.

Если у нас функции с одним входным значением и одним результатом, то они отлично соединяются:

gqaf6oi87auxyuriq1w7p2sj2mi.png

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

-pht73o8rzjd3tnle4yebbvahig.png

Но у нас вот такая ситуация:

jmmd21sqticuu6qzyrxppeaff50.png

У нас одно входное значение, два на выходе и они не соединяются!

Что же нам сделать для решения этой проблемы?

У нас есть один «вход» и два «выхода»:

k6qvn8-g1ftzeaogeh-gzilxdyw.png

А если бы было два «входа» и два «выхода» — это было бы прекрасно, такие конструкции мы можем очень легко соединить:

6fcipihhcslrjswu6dvt38go1vc.png

И нам нужен спосо

© Habrahabr.ru