Почему функциональное программирование?

Сеть наполняется статьями, восхваляющими функциональное программирование (ФП), иногда даже начиная с критики объектно-ориентированного программирования (ООП). Хотя в последнее время эта парадигма начала набирать обороты, в ней нет ничего нового, учитывая, что ей уже более 60 лет. Вы правильно прочитали это число, FP может быть старше вашей мамы. Так почему же вся эта шумиха возникает внезапно? Люди Интернета только что заново изобрели велосипед? Им нужно так много времени, чтобы изучить парадигму программирования? Но сам термин ООП придуман намного позже ФП.

Не то чтобы кто-то мешал нам использовать FP тем временем. Lisp был там все это время, и с ним все в порядке. Однако в последнее время появились целые новые отрасли промышленности, обладающие характеристиками, которые могут извлечь выгоду из подхода FP. У нас есть данные, доступные в большем количестве, чем когда-либо. У нас есть аппаратное обеспечение с несколькими ядрами ЦП, доступное в пределах досягаемости. Искусственный интеллект активно используется в некоторых постоянно растущих областях. Это поля, на которые FP ориентировался с самого начала.

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

Почему PHP?

Помимо этого почему бы и нет?, FP повсюду. Почему PHP должен быть исключением? Можете ли вы сделать FP на C++? Определенно! Как насчет С? Ну, до некоторой степени, конечно. Тем не менее, вы можете в любой момент внедрить Lisp в свой C-код, чтобы выполнять тяжелую работу с FP (у него тоже есть регулярные обновления). Просмотр примечаний к выпуску языков, которые хорошо известны своей парадигмой ООП, таких как Java и C#, похож на чтение отчетов о том, как сами эти языки пытаются наверстать упущенное на фронте FP. Существует огромный спрос на это, поскольку все больше и больше людей понимают FP.

Некоторые из наших любимых инструментов уже реализованы с использованием FP, а с асинхронными, неблокирующими и распределенными системами с каждым днем ​​их становится все больше. Вы когда-нибудь задумывались, почему jQuery, Node.js, Swoole и ReactPHP (среди прочих) иногда имеют несколько нестандартный API, по крайней мере, по сравнению с тем, к чему мы привыкли в ООП? Вот почему.

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

Хотя использование инструментов FP само по себе является доказательством того, что FP можно без проблем смешивать с ООП, в настоящей и последующих статьях мы попытаемся сосредоточиться на том, как мы могли бы использовать FP отдельно. Так что это не обычная статья map-reduce, а скорее серия статей о том, как я могу на самом деле использовать монаду.

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

Это звучит правильно? Тогда давайте начнем с него.

Функциональные эксперименты

Давайте проведем серию экспериментов, в которых мы вместе обнаружим основные особенности ФП. Чем она лучше других парадигм, к которым мы привыкли? Как это отличается? И как это то же самое?

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

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

Если вы все еще следуете, вы можете идти.

Предпосылки

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

Для этого нам нужно знать основные понятия. Что мы и пробежимся прямо сейчас.

Обратите внимание, что все примеры написаны на PHP 8. Хотя некоторые из них могут работать и в более ранних версиях (или могут быть легко преобразованы), из соображений удобства я рекомендую использовать последние функции, предоставляемые языком.

Функции подобраны вручную

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

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

Также не помогает то, что при реализации ООП PHP наследует тот же подход и называет все методом независимо от того, возвращают они значение или нет (хотя в ООП это допустимо). Итак, опять же, FP немного придирчив к функциям и не рассматривает подпрограммы, которые не возвращают значение, как «функции».

Функции из Высокого Замка

С точки зрения FP, так же, как есть «функции», которые недостаточно квалифицированы, чтобы даже называться функцией, также существуют функции, которые пользуются большим уважением, благородством функций — так сказать — благодаря их выдающейся роли. Такие функции, также известные как «функции высшего порядка» (шучу, их на самом деле называют функциями высшего порядка), заслужили такую ​​честь, потому что сами способны обрабатывать функции. Следовательно, любая функция, которая ожидает другую функцию в качестве входных данных или возвращает другую функцию, считается функцией более высокого порядка. Это очень важно, поскольку именно эта характеристика позволяет составлять функции и создавать закономерности.

С этой точки зрения нам снова повезло с PHP. Несмотря на несколько неуклюжий синтаксис, он по-прежнему позволяет нам передавать функции в виде вызываемых или Closure. Кроме того, он даже позволяет передавать методы, функции, анонимные функции и стрелочные функции. Но в чем разница? Помните, что мы имеем дело только с функциями, которые действительно возвращают значение. Так что со всеми этими видами функций мы будем делать только это: возвращать значение.

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

Здесь я намеренно объявил, что makeCallable возвращает string, а не callable, в то время как call_user_func ожидает вызываемый объект в соответствии с документацией, но, поскольку PHP не является строго типизированным языком, эта строка фактически считается вызываемой, если она ссылается на реальную функцию. что можно назвать. В других языках я мог бы использовать это имя напрямую, и интуитивно мы могли бы ожидать того же от PHP, но оказалось, что такая доброта зарезервирована только для констант и не будет работать. Так что продолжайте передавать строки, относящиеся к реальным фрагментам кода, или читайте дальше и используйте что-то совершенно другое.

Closures: на самом деле являются экземплярами класса Closure в PHP, которые реализуют некоторые методы, которые можно использовать для вызова нашей функции и привязки к ней некоторого контекста. Имейте в виду, однако, что PHP не всегда захватывает контекст автоматически, а ожидает, что мы привяжем его явно либо с помощью bind, либо с помощью use (подробности см. в соответствующих частях документации).

Теперь у этого класса есть закрытый конструктор, поэтому никто не будет создавать его таким образом, но анонимные функции и функции стрелок фактически хранятся внутри как экземпляры Closure и, следовательно, не требуют никакого преобразования, в то время как Closure также предоставляет вспомогательный метод для преобразования вызываемые в замыкания следующим образом: Closure::fromCallable($someCallable) — возвращает экземпляр замыкания. Если вы чувствуете себя безопаснее, чтобы обойти Замыкания, вы можете сделать это с помощью этого метода, но у нас есть и другие варианты.

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

Мы только что сделали FP-передачу функций по пути PHP. Но подождите, есть еще!

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

fn() => "I'm pretty much of an arrow function"

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

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

echo wrap(fn($a) => "'$a'", 'c');

В них хорошо то, что все они более или менее совместимы и хорошо служат нашим целям FP.

Отказаться от сопротивления!

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

Кто-то пил из моей чашки!

Кто? Это сводится к принципу неизменности. Это означает, что наши ценности отказываются меняться и сопротивляются попыткам изменить их.

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

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

const c = Math.random();

Прежде всего, PHP не позволяет нам определять значение константы динамически, а скорее ожидает, что мы предоставим скалярные значения. Это не очень полезно.

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

Справедливости ради следует отметить, что эти две проблемы имеют место только тогда, когда мы пытаемся использовать ключевое слово const, в то время как мы можем использовать более неуклюжий синтаксис define:

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

val testName = "test1.txt"

или ржавчина:

let testName = "test1.txt";
let mut proneToChange = 1; // Beware the mutable state!

Чувак, они неизменяемы по умолчанию! Нечего бояться.

Оказывается, помимо define и const есть еще один способ определения неизменяемых значений, и он с самого начала встроен в PHP. Учти это:

function testFileName(): string {
  return "test1.txt";
}

Могу ли я переопределить testFileName, чтобы он указывал на другое значение? Нет, пока это в рамках. Точно так же, как правильное неизменяемое значение (я бы не стал произносить что-то вроде «неизменяемая переменная», поскольку ее значение никогда не меняется). Так что его безопасно использовать с точки зрения изменчивости. Однако этот тип определения имеет довольно накладные расходы, использует больше памяти и излишне определяет функцию, в которой простое значение сделало бы реализацию также излишне относительно медленной.

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

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

«О рисках и побочных эффектах…»

Другой источник неожиданностей возникает, когда мы делегируем задачу определенному фрагменту кода, и он сам занимается другими теневыми делами. Это противоречит нескольким принципам, таким как единая ответственность и разделение ответственности среди прочих. Когда мы полагаемся на что-то, мы ожидаем, что оно сделает что-то одно и сделает это правильно. Это настолько важный вопрос, что программисты не могут перестать подчеркивать его, независимо от парадигмы, которой они следуют (см. принципы SOLID).

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

Мы склонны представлять это как торговый автомат. Мы бросаем монету, и он выплевывает закуски. Действительно, мы были бы очень разочарованы, если бы он отказался дать нам то, за что мы заплатили. Но не только это, мы также не ожидаем от него каких-либо других действий, не связанных строго с выпуском наших товаров. Например, что, если это также аннулирует наши билеты на поезд? Отменяет нашу бронь в отеле. Или трясет нашу газировку как сумасшедшую, чтобы она взорвалась, когда мы ее открываем. Это было бы ужасно!

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

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

Код настолько чистый, насколько это возможно

Я упомянул «чистый» пару раз, но я не упомянул, что это значит. Если парадигма имеет только функции в качестве строительных блоков (мы видели, что даже значения могут быть легко определены как функции), то мы можем представить, что по крайней мере она отличает функции от функций. Действительно, мы рассмотрели некоторые из них, начиная с проблемы, что не все функции называются «функциями», и что не все функции созданы равными.

Есть еще одно: некоторые функции чище, чем другие.

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

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

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

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

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

Здесь шаблон Singleton гарантирует, что у нас всегда будет только один экземпляр нашего класса. Тем не менее, это не мешает нашему экземпляру изменяться. Это похоже на создание константы в JavaScript и предположение, что она никогда не изменится, хотя она все еще желает изменить значения своих свойств:

>> const o = { a: 'a' };
<- undefined
>> o.a = 'b';
<- "b"
>> o.a
<- "b"

У Конста здесь нет силы. Как можно чувствовать себя в безопасности, пока троянцы ждут предательства? Когда мы говорим «неизменный», мы имеем в виду именно это!

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

Лучше, чем остальные

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

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

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