Функциональное программирование в PHP
Функциональное программирование - это парадигма, которая в последнее время приобрела популярность с появлением нескольких новых языков функционального программирования и интеграцией новых функций в существующие языки. Тем не менее, в сообществе разработчиков существует много опасений по поводу функционального программирования. Полезно ли функциональное программирование для разработчика PHP? Добавит ли это сложности в написании кода? В этой статье я попробую ответить на эти вопросы.
Чтобы извлечь максимальную пользу из этой статьи, я надеюсь, что вы в настоящее время немного знаете о функциональном программировании. Кроме того, я предполагаю, что читатель обладает некоторыми знаниями в области объектно-ориентированного программирования, такими как знакомство с принципами проектирования SOLID и знание функций PHP, таких как вызываемые объекты, анонимные функции и методы массива, которые принимают вызываемые объекты, такие как array_map(), array_filter() и array_reduce().
Функциональное и императивное программирование
Первый миф о функциональном программировании заключается в том, что это более старый стиль дообъектно-ориентированного программирования, где инструкции написаны одна за другой, как в шаблоне WordPress. Этот миф, к сожалению, сохраняется из-за недостатка знаний о различных существующих парадигмах программирования. Без этого знания каждая парадигма обычно организуется в сознании программиста как объектно-ориентированная или не объектно-ориентированная.
Этот миф особенно актуален в сообществе PHP, и на то есть веские причины. Те, кто работал в PHP в начале 2000-х (и раньше), не обладали объектно—ориентированными функциями — интерфейсами, пространствами имен и абстрактными классами, чтобы назвать несколько – которые с тех пор я организовал проекты PHP 5+, и это привело к тому, что многие проекты увязли в коде спагетти без четких границ между обязанностями. Эта история приводит к четкому разграничению взглядов программистов между объектно-ориентированным подходом и "старым плохим способом".
Однако функциональное программирование - это не возврат к старому способу ведения дел. Функциональное программирование ставит в центр не описание объектов и их поведения, а описание функций и их входов и выходов. Затем эти функции применяются и составляются для создания нашей системы.
Основная проблема, возникающая с кодом до выпуска PHP 5, заключалась в том, что ему не хватало организованности, сосредоточенности и ясности. Функциональное программирование имеет организующий принцип, который придает ему целенаправленность, и правила, которые делают их явными.
Функции в функциональном программировании в основном можно сравнить с функциями в математике. То есть что-то вроде f (x), где для заданного ввода x выполняется некоторая математика, и каждый раз для одного и того же ввода возвращается один и тот же результат. Эта цель в функциональном программировании известна как написание чистых функций.
Чистая функция - это любая функция, в которой при одном и том же вводе возвращается один и тот же вывод. Возможно, вы уже можете представить себе некоторые последствия. Чистые функции обязательно защищены от побочных эффектов, поскольку любая зависимость от состояния вне функции приведет к изменению выходных данных — или они не будут чистыми функциями.
Без побочных эффектов сами входы и выходы должны быть неизменяемыми, то есть функция не может изменять входные данные, так как это было бы побочным эффектом. Например, если бы у нас была функция, которая принимала дату и вернувшись на следующую дату, мы могли бы реализовать что-то вроде
<?php function nextDay(\DateTime $date) { $interval = new DateInterval('P1D'); return $date->add($interval); } $date = new DateTime('August 12, 1791'); nextDay($date); echo $date->format('F j, Y'); // August 13, 1791Однако этот пример изменяет входные данные, так как PHP передает объекты по ссылке, что может иметь последствия в любом коде, в котором изначально использовался этот объект даты. Чтобы эта функция была чистой, можно реализовать её так, как показано в коде ниже
<?php function nextDay(\DateTime $date) { $interval = new DateInterval('P1D'); $nextDay = clone $date; return $nextDay->add($interval); } $date = new DateTime('August 12, 1791'); $next = nextDay($date); echo $date->format('F j, Y'); // August 13, 1791 echo $next->format('F j, Y'); // August 14, 1791Здесь мы создаем новый объект DateTime и возвращаем новую дату. Ввод остается нетронутым, и вызывающему коду не нужно беспокоиться об обработке побочного эффекта. На самом деле в PHP есть набор неизменяемых объектов данных, которые мы могли бы использовать вместо этого для реализации, как в коде ниже:
<?php function nextDay(DateTimeImmutable $date) { $interval = new DateInterval('P1D'); return $date->add($interval); } $date = new DateTimeImmutable('August 12, 1791'); $next = nextDay($date); echo $date->format('F j, Y'); // August 13, 1791 echo $next->format('F j, Y'); // August 14, 1791Функциональное программирование невозможно без функционального языка
Те, кто отказывается от функционального программирования на PHP по этим причинам, укажут, что PHP не является функциональным языком, таким как Haskell или OCaml. Без основы функционального языка парадигма обречена на провал в контексте PHP. Этот миф правдоподобен, потому что в нем есть доля правды! Функциональное программирование и его теоретические основы в лямбда-исчислении (в которые мы не будем вдаваться в этой статье) являются основой популярных функциональных языков. В то же время PHP будет позволять вам смешивать кусочки парадигмы функционального программирования так, как вам нравится.
Проблема здесь заключается в определении. Каждая парадигма программирования имеет расплывчатые определения и нечеткие границы между ними. Если вы попросите пять разработчиков дать определение объектно-ориентированному программированию, вы получите шесть ответов, семь рекомендаций по книгам и одно сильное желание переехать в лес. То же самое относится и к функциональному программированию. Четкого определения не существует.
Нам нужно принять очень расплывчатое определение функционального программирования на PHP, которое сводится к поддержке первоклассных лямбда-функций. Нам нужны функции, которые можно определять, передавать, применять и составлять по желанию. “Функции первого класса” относятся к способности назначать функцию переменной. Эта языковая функция поддерживает требование о том, чтобы мы могли передавать функции. Он имеет первоклассную поддержку в PHP с 2009 года с выпуском PHP 5.3
<?php //поддержка PHP с 2009 года с выпуском PHP 5.3. // Назначить функцию переменной // Это создает закрытие, назначенное $sum $sum = function($a, $b) { return $a + $b; }; // Вызовем функцию, используя имя переменной echo $sum(5, 4); // 9"Лямбда-функция" - это анонимная функция, то есть ваша функция может быть определена и использована в вашем коде без указания имени. Это соответствует требованию, чтобы функции могли быть определены в любом месте вашего кода, и чаще всего используется в PHP в качестве обратных вызовов. Обратные вызовы предшествовали PHP 5.3 некоторыми способами, такими как call_user_func() с использованием именованных функций. Однако в 2009 году они были дополнительно формализованы как тип с 5.3. Это также указывает на возможность передачи функции в другую функцию. Аналогично, мы можем использовать лямбда-функции для возврата функции из функции.
<?php $names = ["John", "James"]; //Анонимная лямбда-функция, переданная в качестве параметра. array_map( function ($n) { echo "Hello; $n"; }, $names );Хотя PHP не является функциональным языком программирования, это язык общего назначения, который теперь поддерживает несколько парадигм программирования. Мы можем использовать и применять принципы функционального программирования к нашему коду, используя лучшие практики, чтобы решить, когда использовать ту или иную парадигму, так же, как мы используйте опыт и лучшие практики, чтобы определить, когда следует применять шаблон проектирования (паттерн).
Функциональное программирование разрушит мою объектно-ориентированную кодовую базу
Начиная с PHP 5, в PHP наблюдается постоянное стремление к языковым функциям, которые поддерживают лучшие практики объектно-ориентированного программирования. Это привело ко многим значительным достижениям в организации кодовой базы PHP и большей простоте разработки библиотек.
При рассмотрении парадигмы функционального программирования, разработчик может подумать, что он не может объединить её с объектно-ориентированным подходом. Поскольку мир фреймворков PHP полностью объектно-ориентирован, функциональное программирование - это игрушечная идея, которую мы не можем использовать в существующих проектах. Это неправда
Два подхода к обдумыванию и написанию кода могут дополнять и улучшать друг друга. Некоторые из наиболее влиятельных идей, связанных с объектно-ориентированным программированием сегодня, - это SOLID принципы проектирования. Это принцип единой ответственности, принцип открытия/закрытия, принцип замещения Лискова, принцип разделения интерфейсов и принцип инверсии зависимостей. Давайте рассмотрим их и посмотрим, как функциональный подход может помочь нам достичь этих целей
Принцип единой ответственности
Идея принципа единой ответственности заключается в том, что у каждого класса, который мы пишем, должна быть только одна причина для изменения. Если у класса более одной ответственности, это изменение становится более обременительным. Изменение одной из обязанностей вашего класса может повлиять на другие обязанности вашего класса, что повысит вероятность появления ошибок.
Мышление в терминах лямбда-функций помогает принципу единой ответственности на уровне метода / функции. Используя лямбда-функции, мы можем разбить наш код на его мельчайшие детали, одновременно перекладывая второстепенные обязанности на другие объекты или методы.
Давайте посмотрим на некоторый код, который находит самый большой продукт в серии цифр.
function largestProduct(string $digits, int $len) { $factorsArray = []; for ($i = 0; $i <= strlen($digits) - $len; $i++) { $factorsArray[] = substr($digits, $i, $len); } $productsArray = []; foreach ($factorsArray as $factors) { $productsArray[] = array_product(str_split($factors)); } return max($productsArray); }Мы видим, что здесь смешано несколько обязанностей. Я строю массив коэффициентов из заданного ряда цифр. Затем я получаю результаты каждого из этих факторов. Все это можно превратить в отдельные функции, как вы можете видеть в другом коде:
function largestProduct(string $digits, int $len) { return max(array_map( 'getProduct', getFactors($digits, $len) )); } function getFactors(string $digits, int $len) { return array_map( fn ($i) => substr($digits, $i, $len), range(0, strlen($digits) - $len) ); } function getProduct(string $factors) { return array_product(str_split($factors)); }Теперь мы немного переработали, и каждая из наших переработанных функций выполняет только одну вещь. Это решение делает обширные использование функций массива PHP, которые, вероятно, являются наиболее распространенными вариантами использования, которые вы найдете для мышления функционального программирования.
Принцип "Открыто/Закрыто"
Принцип открытости/закрытости в объектно-ориентированном программировании диктует, что объекты должны быть открыты для расширения, но закрыты для модификации. Идея здесь состоит в том, чтобы помочь изменениям кода, избегая хрупкости, возникающей в результате кодовых баз, где классы напрямую зависят от других конкретных классов, поэтому изменение класса для завершения запроса функции может привести к непредвиденным ошибкам в другом.
На практике разработчики понимают это как означающее, что классы должны зависеть от абстрактных классов или интерфейсов. Вместо того, чтобы нуждаться в новых деталях реализации в вашем классе для каждого конкретного поддерживаемого класса ваш класс будет полагаться на абстракцию. Затем он мог бы использовать конкретные классы, которые расширяют эту абстракцию или реализуют этот интерфейс без необходимости изменений.
Таким образом, вы расширяете функциональность своего класса без необходимости внесения изменений для поддержки новых требований к функциям. Однако, если мы расширим наше мышление о принципе открытости/закрытости, включив в него концепции функционального программирования, мы сможем увидеть другие способы применения принципа открытости/закрытости.
Функции более высокого порядка, то есть функции, которые могут принимать другие функции в качестве входных данных или функции возврата в качестве выходных данных также открыты для расширения и закрыты для модификации. Некоторые из лучших примеров этого уже являются собственными функциями PHP. Набор функций массива, которые принимают обратный вызов функции, - это все функции более высокого порядка, функциональность которых расширяется передаваемой вами функцией. В то же время вы также не можете изменить внутреннюю работу этой функции.
Неизменяемые объекты, по определению, полностью закрыты для модификации после их создания. Их внутреннее состояние не может быть изменено, и любой метод, который действительно вносит “изменение”, фактически создаёт новый неизменяемый объект.
Это довольно далеко от первоначального определения принципа открытости/закрытости или даже его интерпретации как зависимости от абстракции. Тем не менее, неизменность сохраняет важную роль в функциональном программировании. Неизменяемость помогает поддерживать создание чистых функций, которые не имеют побочных эффектов, поскольку неизменяемые объекты защищают программиста от создания побочных эффектов в объектах, которые они используют. Таким образом, мы могли бы рассматривать это как естественное согласование целей по принципу "открыто/закрыто".
Принцип замещения Лисков
Принцип подстановки Барбары Лисков означает, что дочерние классы должны поддерживать поведение своих родительских классов. Одним из побочных продуктов принципа замещения Лисков в современных передовых практиках является избежание иерархий наследования, в которых этот принцип призван помочь вам полностью ориентироваться. Вы можете видеть это в распространении библиотек внедрения зависимостей и фреймворков, которые их используют.
Наследование не является частью функционального программирования. Как указывалось ранее, функциональное программирование основано на композиции функций: построение поведения путем объединения других поведений / функций. Это удивительно похоже на идею внедрения зависимостей, к которым программисты обратились для удовлетворения требований LSP.
Принцип инверсии зависимостей
Принцип инверсии зависимостей гласит, что абстракции не должны зависеть от деталей, а детали не должны зависеть от абстракций. В объектно-ориентированной парадигме мы понимаем это как означающее, что классы должны отделять бизнес-логику высокого уровня от кода, зависящего от интерфейсов или контрактов, а не от конкретных классов.
Опять же, мы можем обратиться к функциям более высокого порядка, которые могут принимать функцию в качестве входных данных и применять ее. В этом случае интерфейс это сама функция, а детали реализации содержатся в функции. Это возможно благодаря первоклассным функциям.
Набор функций array_* в стандартной библиотеке PHP снова показывает конкретный пример, где детали реализации вашей функции фильтра или карты оставлены на ваше усмотрение. Напротив, функциональность более высокого уровня применения этой функции к массиву содержится в стандартной библиотечной функции.
Как функциональное программирование может вписаться в мою объектно-ориентированную кодовую базу?
Итак, как выглядит функциональное программирование в действии в кодовой базе? Особенно в объектно-ориентированной кодовой базе? В другой среде, такой как виртуальная машина Java или экосистема .NET, программист может перейти на функциональный язык, такой как Scala или F#, для реализации частей своего кода , на которые влияет мышление функционального программирования. Однако нам нужна другая стратегия в PHP, поскольку у нас есть функции , поддерживающие функциональное программирование, но нет специального инструмента для этого.
Можно например использовать подход Гэри Бернхардта, при котором основная бизнес-логика написана в функциональном стиле. Этот план означает , что объекты неизменяемы, а поведение записывается как чистые функции, которые принимают некоторый ввод и возвращают новый объект. Затем эти объекты склеиваются тонкой оболочкой императивного кода, который будет взаимодействовать с внешним миром, включая пользовательский ввод, API или любые другие проблемы, которые у вас есть.
Как бы это выглядело в современном фреймворке PHP, таком как Laravel? Классы контроллера Laravel и запросов могут содержать императивную оболочку, в то время как бизнес-логика находится в простых объектах PHP.
Одним из побочных эффектов написания объектов в функциональном стиле является положительное влияние на тестирование. Поскольку ваша основная бизнес-логика теперь определена в чистых функциях, их можно тестировать с помощью модульных тестов, основанных только на входах и выходах, при этом требуется небольшая настройка. Это может даже открыть вашу кодовую базу для использования инструменты математического тестирования, такие как quickcheck4.
Программирование пользовательского интерфейса
Программирование пользовательского интерфейса в парадигме функционального программирования принимает форму функционального реактивного программирования (FRP). Идея FRP состоит в том, чтобы перейти к модели, полностью основанной на событиях, и позволить пользовательскому интерфейсу использовать неизменяемые объекты и чистые функции для изменения пользовательского интерфейса.
Мы должны противопоставить это императивной модели разработки пользовательского интерфейса, которая в основном принимает форму поддержания состояния и создания поведения на основе этого состояния. FRP требует среда выполнения, управляемая событиями, например, предоставляемая библиотекой React PHP. В конце концов, вы не можете реагировать на события без событий!
Если ваш пользовательский интерфейс написан на Javascript в виде одностраничного приложения, для вас открыты дополнительные возможности, такие как RxJS. Существуют даже способы включения методов FRP в приложения React и Vue.
Заключительные замечания
Давайте рассмотрим некоторые мифы, окружающие функциональное программирование, и их разоблачения. Функциональное программирование - это не то же самое, что императивное программирование. Функциональное программирование структурировано, как и объектно-ориентированное программирование, но нацелено на другие философские основы. Основы функционального программирования включают неизменяемость, чистые функции и составность этих чистых функций.
Вам не нужно работать на функциональном языке программирования, чтобы извлечь выгоду из парадигмы функционального программирования. В то время как такой язык, как OCaml или Haskell может заставить вас заняться функциональным программированием, у PHP есть языковые возможности для вашей поддержки. Функции первого класса, то есть функции, которые могут быть назначены переменным и переданы другим, доступны в PHP.
Лямбда-функции являются анонимными, безымянными функциями и также доступны в PHP. Возможно, вы уже используете их в качестве вызываемых объектов в функциях массива или call_user_func().
Лучшие практики объектно-ориентированного программирования не являются несовместимыми с функциональным программированием. SOLID принципы проектирования, которые формируют основу столь многих объектно-ориентированных программ, могут быть дополнены и усовершенствованы мышлением функционального программирования.
Поскольку принципы проектирования SOLID заставляют вас уточнять определения классов до более мелких фрагментов поведения, вы можете даже столкнуться с классом, который представляет собой тонкую оболочку вокруг одной части поведения в методе. Это не так уж сильно отличается от чистой функции!
В качестве метода для включения функционального программирования в ваш код можно использовать императивную оболочку вокруг функционального ядра бизнес-логики. Это не только требует применения некоторых SOLID принципов проектирования, но это также оказывает положительное влияние на модульное тестирование. Разработчики могут даже использовать функциональное программирование в пользовательском интерфейсе с помощью функционального реактивного программирования.
Надеюсь, что эта статья поможет развеять не только некоторые предвзятые представления о функциональном программировании, но и покажет, как использовать функциональное программирование в существующем коде, в текущей работе. Как разработчики, мы можем признать, что различные точки зрения обостряют наш анализ проблемы и помогают нам писать код, которая решает проблему, будучи при этом более понятным и поддерживаемым.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.
Читайте также:
Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.