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

Приправить ваш код

Хотя это звучит модно, карри на самом деле не имеет ничего общего с вашей любимой смесью куркумы, а получил свое название от американского математика Хаскелла Карри (вы обязательно запомните это имя). Тем не менее, мы увидим, что каррирование обеспечивает не что иное, как оживление нашего кода.

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

Это карри. (На самом деле это автоматическое каррирование, потому что вам не нужно было явно вызывать curry для функции, как мы.)

Мы можем проиллюстрировать это с помощью некоторой символики следующим образом.

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

<input_type> → <output_type>

И функции, принимающие несколько аргументов, например:

<input_type_1>, <input_type_2>, <input_type_3> → <output_type>

Затем мы можем представить каррированные функции следующим образом (например, для 3 аргументов):

<input_type> → (<input_type_2> → (<input_type_3> → <output_type>))

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

Сколько здесь входных аргументов, не важно.

<input_type_1>, ...<input_type_n> → <output_type>

будет карри:

<input_type_1> → (... → (<input_type_n> → <output_type>))

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

<input_type_1> → ... → <input_type_n> → <output_type>

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

На практике это означает, что если вы ожидаете 5 аргументов и не указали ни одного, вы получите функцию, которая будет ожидать 5 аргументов или меньше. Если вы передали этой функции только 1 аргумент, она вернет новую функцию, ожидая оставшиеся 4 аргумента, при этом сохраняя тот, который уже был предоставлен. И если вы скармливаете этой функции 2 аргумента, возвращающая функция будет ожидать оставшиеся 3 аргумента и так далее. Хорошо то, что возвращенная функция также окажет нам эту услугу, и мы можем кормить функцию аргументами один за другим. Или по два. Или сгруппировать как хотите.

Что, если бы вы предоставили все необходимые аргументы? Затем ваша функция, наконец, выполняется, как мы видели выше, и возвращает свой результат.

Это очень мощная концепция, позволяющая избавиться от значительного количества локальных переменных.

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

А пока я расскажу о списках аргументов переменной длины. Увидимся через минуту.

«Удав, переваривающий слона»

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

Это делается путем пометки наших функций маркером ..., как их называет PHP. На других языках это называется многоточие (C / C ++, Java) или оператор распространения (JavaScript / TypeScript), но PHP, как ни странно, избегает называть его любым из этих имен и использует ... token или ... операторские фразы («анонс релиза) в соответствующих частях документации. Даже если мы не называем это по имени (по каким-то суеверным причинам), они очень полезны, и мы будем ими пользоваться довольно часто.

Синтаксис выглядит следующим образом:

function variadic(...$arguments) {}

где параметр $arguments будет array. Это может помочь нам в PHP одновременно снабжать наши функции произвольным числом аргументов и упростить некоторый код.

Вернемся к нашему супу ...

Готовим с карри

Наша первая попытка использовать нашу функцию карри (которую вы либо уже реализовали, либо будете использовать мою реализацию, которую я предоставлю позже) может выглядеть примерно так:

Если мы запустим этот код, он должен напечатать:

myc(a, b, c)     =>     a b c
myc(a)(b)(c)     =>     a b c
myc(a, b)(c)     =>     a b c
myc(a)(b, c)     =>     a b c
myca(b, c)       =>     a b c
myca(b)(c)       =>     a b c
mycab(c)         =>     a b c
mycabc           =>     a b c

Если бы мы выбрали подход к разработке через тестирование, это могло бы быть нашим (небрежным) тестом. Мы создали каррированную версию нашей простой функции $myfunc (строка 5), три другие функции, передавая им аргументы один за другим (строки 7–9), а затем начали вызывать эти функции как сумасшедшие.

Наша гипотетическая функция curry, очевидно, принимает только один аргумент - функцию, которую нужно каррировать.

$myfunc будет нашей исходной функцией, которая имеет фиксированное количество параметров и просто создает из них другую строку. Здесь не происходит никакого волшебства.

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

Строки, выводимые на экран, показывают различные способы вызова таких каррированных функций. Как видите, независимо от того, как мы их называем или как группируем аргументы, они всегда возвращают одну и ту же строку, как только все аргументы были предоставлены: a b c.

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

curry($myfunc)('a', 'b', 'c')

По определению, он вернет тот же результат: a b c. Так почему бы не использовать нашего друга с тремя точками и принять также список аргументов переменной длины? Ну, это зависит от обстоятельств. Предположим, он у нас есть, и мы можем совершать такие звонки:

curry($myfunc, 'a', 'b', 'c'); // 1.
curry($myfunc, 'a')('b', 'c'); // 2.

Так лучше? Я не вижу смысла в первом, потому что мы могли бы вызвать нашу исходную функцию напрямую. Если curry возвращает результат исходной функции, то есть. Но если он все время возвращает функцию (вызываемую), нам нужен еще один вызов для запуска исходной функции, например:

curry($myfunc, 'a', 'b', 'c')();

Для меня это тоже не имеет большого смысла. Единственная причина, по которой я могу это сделать, - это убедиться, что наша функция curry всегда возвращает вызываемый объект.

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

curry(callable $fn)

Ни шума, ни беспорядка, только самое необходимое. Хорошо, продолжай.

Сервировка блюд

Нельзя просто фотографировать еду, если еще хочется пробудить аппетит. Оказывается, сервировка играет в этом решающую роль. Как еда размещается на тарелке, как она освещается, какая камера используется и т. Д.

В приведенном выше примере использования мы можем найти 23 экземпляра символа $. В 18 строках кода. Это более 3% от общего числа персонажей. Значительный беспорядок. Как упоминалось ранее, они нам даже не нужны. Мы были бы счастливы, если бы вместо них были константы, но наши возможности для этого также не очень яркие.

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

function immutable(): string { return 'totally immutable'; }

Раньше мы определяли переменные для хранения функций. Итак, давайте определим и их неизменными:

function immutableFn(): callable { return fn() => 'yeah'; }

Давайте посмотрим, как мы можем использовать каррирование с этим.

Выходы:

Immutable greeting: Good day, Tülin!
Good day, Tülin!
Hello, Tülin!

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

У нас есть неизменное значение для нашей основной функции greet. Он просто возвращает функцию, которая принимает два аргумента, форму и имя.

У нас есть два неизменных значения для двух поддерживаемых форм, formal и informal со значениями «Добрый день» и «Привет» соответственно.

Затем мы определили два новых неизменяемых значения, используя некоторую магию FP, снабдив нашу исходную greet функцию предопределенными значениями, исходящими из formal и informal, и присвоив им имена greetFormally и greetInformally.

Важный вывод здесь заключается в том, что мы можем передавать данные нашим функциям, когда они становятся доступными, и что мы можем внедрять функции в другие функции аналогично внедрению зависимостей или шаблону разработки стратегии. Это помогает нам разделить проблемы, не повторяться (СУХОЙ) и сохранять единоличную ответственность. Да, все это ООП, просто проще.

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

Наконец, мы определяем другой неизменяемый объект со значением «Tülin», называемый tulin, и еще один, называемый girl, со значением, уже сохраненным в tulin.

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

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

greet()(formal(), tulin())

а также

greetFormally()(tulin())

На первый взгляд они могут показаться странными, но в идеальном мире они выглядели бы так:

greet(formal, tulin)

а также

greetFormally(tulin)

Этот идеальный мир похож на то, как работает синтаксис Scala. Итак, как бы весь фрагмент кода выглядел на воображаемом языке PHP?

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

let callable greet = fn(callable $form, string $name): string => $form() . ', ' . $name . '!';
let callable formal = fn(): string => "Good day";
let callable informal = fn(): string => "Hello";
let callable greetFormally = curry(greet)(formal);
let callable greetInformally = curry(greet)(informal);
let string tulin = "Tülin";
let string girl = tulin;
// reassignment does not work:
// let string girl = "Somebody else";
greet(formal, tulin);
greetFormally(tulin);
greetInformally(girl);

Лучше! А теперь перестань мечтать и вернись к работе. У нас есть функция, которую нужно реализовать.

Реализация карри в PHP

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

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

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

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

Мы используем классную функцию отражения PHP (строка 4), которая предоставляет некоторую метаинформацию об используемых вещах, например, сколько параметров имеет наша функция. Это также позволяет вызывать нашу функцию с некоторыми аргументами (строка 16).

Наша основная функция карри реализована здесь как частный метод. Открытый метод предоставляет только понятный интерфейс и выполняет некоторую подготовку. _curry может быть рекурсивной функцией, то есть вызывать саму себя, но нас это не волнует. Мы просто позволяем пользователю вызывать его, если он им нужен (строка 14). В нашем случае пока не приведены все аргументы. Если ничего не пропало, то просто вызывает исходную функцию с собранными аргументами и возвращает результат (строка 16).

Остальное - это просто PHP mambo jambo: как мы передаем данные по списку параметров и как путем захвата контекста с помощью стрелочной функции (строка 14). Также в этой строке мы заявляем, что не знаем, сколько аргументов будет передано нашей функции в следующий раз, но что бы ни было предоставлено, просто отправьте его нашему личному штангисту карри с помощью оператора распаковки массива ..., который мы не вызываем. по имени.

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

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

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

Это все, что нужно. Убедитесь, что у вас есть рабочая версия curry, протестированная на соответствие предоставленным вариантам использования, потому что с этого момента мы будем активно ее использовать.

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