Скачать исходные файлы
Сейчас поднята большая шумиха вокруг парадигм функционального программирования. Функциональные языки используются все больше и больше в более значительных и интересных приложениях.
Scala, Haskell и другие функциональные языки программирования процветают, а другие, более консервативные языки, такие как Java, начали перенимать некоторые парадигмы функционального программирования (смотрите замыкания в версии Java7 и отложенные вычисления для списков в Java8).
Однако мало кто знает, что PHP достаточно универсален, когда речь заходит о функциональном программировании. Все основные идеи функционального программирования могут быть реализованы на PHP. Поэтому если вы новичок в этом, будьте готовы к взрыву мозга. Если же вы знакомы с функциональным программированием — готовьтесь славно повеселиться в этом уроке.
Парадигмы программирования
Если бы не было никаких парадигм программирования, мы могли бы делать все, что захотим, любым способом, каким нам вздумается. Несмотря на то, что это могло бы привести к чрезвычайной гибкости, это также может привести к нереализуемой архитектуре и очень раздутому коду.
Таким образом, парадигмы программирования были изобретены, чтобы помочь нам, программистам, думать определенным образом об определенной программе и ограничивать наши возможности по выражению нашего решения.
Каждая парадигма программирования отнимает часть нашей свободы:
- Модульное программирование лишает нас неограниченного размера программы;
- Структурное и процедурное программирование лишает оператора «go-to» и ограничивает программиста путем использования последовательного исполнения, ветвлений и циклов;
- Объектно-ориентированное программирование отнимает указатели на функции;
- Функциональное программирование лишает нас возможности использовать присвоение и изменяемые состояния.
Принципы функционального программирования
В функциональном программировании у вас нет данных, представленных с помощью переменных.
В функциональном программировании все является функцией. Да, именно все. Например, множество элементов, как в математике, может быть представлено как несколько функций. Массив или список тоже является функцией или группой функций.
В объектно-ориентированном программировании все элементы являются объектами. А объект, в свою очередь, это совокупность данных и методов, которые взаимодействуют с этими данными. Объекты имеют состояние, переменное, изменяемое состояние.
В функциональном программировании у вас нет данных, представленных с помощью переменных. Нет контейнеров данных.
Данные не присваиваются переменным. Некоторые величины могут быть определены и присвоены, однако в большинстве случаев они являются функциями, присвоенными «переменным». Я поместил «переменные» в кавычки, потому что в функциональном программировании они не изменяются.
Хотя большинство функциональных языков программирования не навязывают неизменность, точно так же, как и большинство объектно-ориентированных языков не обеспечивают использование объектов, если вы изменяете значение после присвоения, то вы отошли от чисто функционального программирования. Так как нет значений, присвоенных переменным, в функциональном программировании нет и состояний.
Так как нет состояний и присвоений, функции не имеют побочного эффекта. И по этим трем причинам функции в функциональном программировании всегда предсказуемы. Проще говоря, это значит, что если вы вызываете функцию с одними и теми же параметрами снова, и снова, и снова, вы всегда получите одинаковый результат. Это огромное преимущество перед объектно-ориентированным программированием, и оно значительно сокращает сложность многопоточных и массово-многопоточных приложений.
Но если мы хотим выразить все через функции, нам необходимо иметь возможность присваивать их параметрам или возвращать их из других функций. Таким образом, функциональное программирование требует поддержку функций высшего порядка. Это значит, что функция может быть присвоена «переменной», передана как параметр в другую функцию, и возвращена как результат функции.
И, наконец, так как мы не присваиваем значения переменным, циклы while и for нетипичны для функционального программирования и заменяются на рекурсию.
Покажите мне код!
Хватит разговоров и философии для одного урока. Давайте программировать!
Создайте новый PHP проект в своей любимой среде разработки или редакторе кода. Создайте в нем папку «Tests». Затем создайте два файла: в папке проекта — файл FunSets.php и в папке «Tests» — FunSetsTest.php. Мы напишем приложение, с тестами, которое будет представлять концепцию множеств.
В математике множество — это совокупность отдельных объектов, рассматриваемая как единый объект.
То есть множество — это группа вещей, локализованных в одном месте. Такие множества могут быть охарактеризованы математическими операциями, такими как объединение, пересечение, разница и т.д., а также таким свойством, как принадлежность к множеству.
Наши ограничения в программировании
Итак, давайте начнем! Хотя нет, подождите. Зачем? Для того чтобы проявить уважение к концептам функционального программирования мы должны применить следующие ограничения к нашему коду:
- Никаких присвоений. Нам не разрешается присваивать значения переменным. Однако можно присваивать функции переменным;
- Никаких изменяемых состояний. Нам не разрешается, в случае присвоения, изменять значение того, что присвоено. Также не разрешается изменять значение любой переменной, значение которой установлено как параметр для текущей функции. Таким образом, нельзя изменять параметры;
- Нельзя использовать циклы while и for. Нам не разрешено пользоваться PHP командами «while» и «for». Зато мы можем определить наш собственный метод, чтобы организовать цикл по элементам множества и назвать его foreach, или for, или while.
Ограничения не применяются к тестам. Учитывая природу PHPUnit, мы будем использовать для тестирования объектно-ориентированный код на PHP. Кроме того, чтобы лучше разместить наши тесты, мы упакуем весь рабочий код в отдельный класс.
Функция, определяющая множество
Если вы опытный программист, но не знакомы с функциональным программированием, сейчас пришло самое время перестать думать так, как вы это обычно делаете, и быть готовым покинуть свою зону комфорта. Забудьте все привычные подходы к решению проблем и представьте все в функциях.
Определяющая множество функция — это метод contains.
function contains($set, $elem) {
return $set($elem);
}
Нельзя сказать, что здесь все очевидно, поэтому давайте посмотрим, как мы могли бы использовать эту функцию.
$set = function ($element) {return true;};
contains($set, 100);
Теперь идея немножко проясняется. Функция contains имеет два параметра:
- $set — представляет множество, определенное как функция;
- $elem — представляет собой элемент, определенный как значение.
В данном контексте все, что метод contains должен делать — это вызвать функцию $set с параметром $elem.
Давайте упакуем все в тест.
class FunSetsTest extends PHPUnit_Framework_TestCase {
private $funSets;
protected function setUp() {
$this->funSets = new FunSets();
}
function testContainsIsImplemented() {
$set = function ($element) {return true;};
$this->assertTrue($this->funSets->contains($set, 100));
}
}
И поместим наш рабочий код в класс в файле FunSets.php:
class FunSets {
public function contains($set, $elem) {
return $set($elem);
}
}
Вы можете даже запустить этот тест и он выполнится. Множество, которое мы определили для этого теста, это просто функция, всегда возвращающая значение true. Это «true множество».
Множество с единственным элементом
Если предыдущий раздел был немного запутанным или выглядел логически бесполезным, то данный раздел должен внести ясность. Мы хотим определить множество с единственным элементом, или синглетон. Помните, это должна быть функция, и мы хотим использовать ее в тесте, приведенном ниже.
function testSingletonSetContainsSingleElement() {
$singleton = $this->funSets->singletonSet(1);
$this->assertTrue($this->funSets->contains($singleton, 1));
}
Нам нужно определить функцию под названием singeltonSet с параметром, представляющим собой элемент множества. В тесте это число один (1).
Затем мы ожидаем, что наш метод contains, вызванный для синглетон функции, вернет значение true, если отправленный параметр равен единице. Код для тестирования выглядит следующим образом:
public function singletonSet($elem) {
return function ($otherElem) use ($elem) {
return $elem == $otherElem;
};
}
Вот это да! Просто очуметь.
Итак, функция singletonSet принимает в качестве параметра элемент $elem. Затем она возвращает другую функцию, которая имеет входной параметр $otherElem и сравнивает $elem с $otherElem.
Как это все работает? Во-первых, вот эта строка:
singleton = $this->funSets->singletonSet(1);
преобразуется в соответствии с тем, что возвращает вызов singletonSet(1):
$singleton = function ($otherElem) {
return 1 == $otherElem;
};
Затем происходит вызов функции contains($singleton, 1). Которая, в свою очередь, вызывает $singleton. Таким образом, код сводится к:
$singleton(1)
Который фактически выполняет код со значением $otherElem равным единице.
return 1 == 1
Что, конечно же, возвращает значение true, и наш тест успешно пройден.
Вы уже улыбаетесь? Вы чувствуете, как ваш мозг начинает закипать? Я точно это чувствовал, когда сначала написал этот пример на языке Scala, и почувствовал снова, когда сделал это на PHP. Я думаю, это нечто необычное.
Нам удалось определить множество с одним элементом и с возможностью проверять то, что это множество содержит величину, которую мы передали в него. Мы сделали все это без единого присвоения значения.
У нас нет переменных, содержащих единицу или имеющих состояние единицы. Нет состояний, нет присвоений, нет изменчивости, нет циклов. Тут мы на верном пути.
Объединение множеств
Теперь, когда мы можем создать множество с единственным значением, мы должны быть способны создавать множество с несколькими значениями. Очевидный способ осуществить это — определить операцию объединения наших множеств.
Объединение двух множеств с единственным элементом будет представлять собой множество с обоими значениями. Мне бы хотелось, чтобы вы нашли минутку и подумали о решении до того, как перешли к рассмотрению кода, возможно, взглянув на тесты, приведенные ниже.
function testUnionContainsAllElements() {
$s1 = $this->funSets->singletonSet(1);
$s2 = $this->funSets->singletonSet(2);
$union = $this->funSets->union($s1, $s2);
$this->assertTrue($this->funSets->contains($union, 1));
$this->assertTrue($this->funSets->contains($union, 2));
$this->assertFalse($this->funSets->contains($union, 3));
}
Нам нужна функция с именем union, которая принимает два параметра, оба являются множеством. Помните, что множества это просто функции для нас, так что наша функция union будет принимать в качестве параметров две функции.
Затем мы хотим иметь возможность проверять при помощи функции contains содержит ли объединение элемент или нет. Таким образом, наша функция union должна возвращать другую функцию, которую можно использовать для вызова contains.
public function union($s1, $s2) {
return function ($otherElem) use ($s1, $s2) {
return $this->contains($s1, $otherElem) || $this->contains($s2, $otherElem);
};
}
Этот код действительно работает достаточно хорошо. И он отлично подходит даже для случая, когда функция union вызывается для другой union плюс синглетон. Она содержит в себе вызов функции contains для каждого параметра. Если параметр — union, то происходит рекурсивный вызов функции. Все просто!
Пересечение и разница
Мы можем применить ту же одностроковую логику с незначительными изменениями, чтобы получить следующие две наиболее важные функции, характеризующие множество: пересечение — содержит только общие элементы двух множеств, и разницу — содержит только те элементы, которые входят в первое множество и не являются частью второго.
public function intersect($s1, $s2) {
return function ($otherElem) use ($s1, $s2) {
return $this->contains($s1, $otherElem) && $this->contains($s2, $otherElem);
};
}
public function diff($s1, $s2) {
return function ($otherElem) use ($s1, $s2) {
return $this->contains($s1, $otherElem) && !$this->contains($s2, $otherElem);
};
}
Я не буду загружать вас кодом для тестирования двух этих методов. Тесты написаны, и вы можете проверить их, если посмотрите прилагаемый к статье код.
Фильтр на множестве
Вот это уже немного сложнее, мы не можем решить это с помощью одной строки кода. Фильтр — это функция, которая использует два параметра: множество и функцию фильтрации.
Фильтр применяет функцию фильтрации к множеству и возвращает другое множество, которое содержит только те элементы, которые удовлетворяют этой функции. Чтобы лучше понять, как это работает, посмотрите тест:
function testFilterContainsOnlyElementsThatMatchConditionFunction() {
$u12 = $this->createUnionWithElements(1, 2);
$u123 = $this->funSets->union($u12, $this->funSets->singletonSet(3));
$condition = function($elem) {return $elem > 1;};
$filteredSet = $this->funSets->filter($u123, $condition);
$this->assertFalse($this->funSets->contains($filteredSet, 1), "Should not contain 1");
$this->assertTrue($this->funSets->contains($filteredSet, 2), "Should contain 2");
$this->assertTrue($this->funSets->contains($filteredSet, 3), "Should contain 3");
}
private function createUnionWithElements($elem1, $elem2) {
$s1 = $this->funSets->singletonSet($elem1);
$s2 = $this->funSets->singletonSet($elem2);
return $this->funSets->union($s1, $s2);
}
Мы создаем множество с тремя элементами: 1, 2, 3. И помещаем его в переменную $u123, чтобы легче распознавать его в наших тестах. Затем мы определяем функцию, к которой хотим применить тест, и помещаем ее в $condition.
Наконец, мы вызываем функцию фильтрации для нашего множества $u123 вместе с $condition и помещаем получившееся множество в $filteredSet. Затем мы запускаем проверку при помощи contains, чтобы убедиться, действительно ли множество выглядит так, как мы хотели.
Наша условная функция проста, она возвращает true, если элемент больше единицы. Поэтому наше конечное множество должно содержать только значения два и три, и именно это мы и проверяем в нашем коде.
public function filter($set, $condition) {
return function ($otherElem) use ($set, $condition) {
if ($condition($otherElem))
return $this->contains($set, $otherElem);
return false;
};
}
Пожалуйста, готово! Мы реализовали фильтрацию с помощью всего трех строк кода. Точнее, если условие выполняется для рассматриваемого элемента, то мы запускаем функцию contains на множестве для этого элемента. Если не выполняется — просто возвращаем false. Вот и все.
Цикл по элементам
Следующий шаг — это создание различных функций циклов. Самая первая — forall() — будет принимать в качестве параметров $set и $condition и возвращать значение true, если $condition применимо ко всем элементам $set. Это приводит к следующему тесту:
function testForAllCorrectlyTellsIfAllElementsSatisfyCondition() {
$u123 = $this->createUnionWith123();
$higherThanZero = function($elem) { return $elem > 0; };
$higherThanOne = function($elem) { return $elem > 1; };
$higherThanTwo = function($elem) { return $elem > 2; };
$this->assertTrue($this->funSets->forall($u123, $higherThanZero));
$this->assertFalse($this->funSets->forall($u123, $higherThanOne));
$this->assertFalse($this->funSets->forall($u123, $higherThanTwo));
}
Мы выделяем создание $u123 из прошлого теста в частный метод. Затем мы определяем три разных условия: больше, чем ноль; больше, чем один; больше чем два. Так как наше множество содержит числа один, два и три, то только условие «больше, чем ноль» должно вернуть значение true, а остальные — false. В действительности, мы можем выполнить тест при помощи другого рекурсивного метода, используемого для перебора всех элементов.
private $bound = 1000;
private function forallIterator($currentValue, $set, $condition) {
if ($currentValue > $this->bound)
return true;
elseif ($this->contains($set, $currentValue))
return $condition($currentValue) && $this->forallIterator($currentValue + 1, $set, $condition);
else
return $this->forallIterator($currentValue + 1, $set, $condition);
}
public function forall($set, $condition) {
return $this->forallIterator(-$this->bound, $set, $condition);
}
Начнем с определения некоторых ограничений для нашего множества. Значения элементов должны быть в диапазоне от -1000 до +1000. Это разумное ограничение накладывается для того, чтобы сохранить пример достаточно простым. Функция forall вызывает private-метод forallItertor с нужными параметрами, чтобы рекурсивно решить, удовлетворяют ли все элементы условию.
В этой функции мы, прежде всего, проверяем, не вышли ли мы за границы. Если да, возвращаем true. Затем проверяем, содержит ли наше множество текущую величину, если да, то возвращаем результат применения логического «И» к проверке текущей величины на выполнение условия и рекурсивного вызова самой функции со следующим значением. В противном случае, просто вызываем из функции саму себя со следующим значением и возвращаем результат.
Все замечательно работает. Мы можем реализовать подобным образом функцию exists(). Она возвращает значение true, если какой-либо элемент множества удовлетворяет условию.
private function existsIterator($currentValue, $set, $condition) {
if ($currentValue > $this->bound)
return false;
elseif ($this->contains($set, $currentValue))
return $condition($currentValue) || $this->existsIterator($currentValue + 1, $set, $condition);
else
return $this->existsIterator($currentValue + 1, $set, $condition);
}
public function exists($set, $condition) {
return $this->existsIterator(-$this->bound, $set, $condition);
}
Единственная разница в том, что мы возвращаем значение false, если вышли за границы, и используем логическое «Или» вместо «И» во втором операторе if.
А теперь наступило время функции отображения map(), отличающейся от предыдущих, более простой и короткой.
public function map($set, $action) {
return function ($currentElem) use ($set, $action) {
return $this->exists($set, function($elem) use ($currentElem, $action) {
return $currentElem == $action($elem);
});
};
}
Отображение обозначает, что мы применяем действие ко всем элементам множества. Для отображения нам не нужна помощь итератора, мы можем повторно воспользоваться exists() и вернуть те элементы exists(), которые удовлетворяют результату $action, примененного к $element. Это, возможно, не совсем очевидно на первый взгляд, поэтому давайте посмотрим, как все происходит.
- Мы передаем множество {1, 2} и действие $element * 2 (удвоение) в функцию map;
- Она вернет функцию, что очевидно, которая имеет в качестве параметра элемент и использует множество и действие с уровня выше;
- Эта функция вызовет функцию exists со множеством { 1, 2 } и условной функцией $currentElement, равной $elem * 2;
- Функция exists() переберет все элементы от -1000 до +1000 (границы нашего множества). Когда она найдет элемент, равный удвоенному значению от того, что пришло из функции contains (значение $currentElement), то функция вернет true;
- Другими словами, последнее сравнение вернет значение true для вызова функции contains со значением два, если текущее значение, умноженное на два, равно двум.
Т.е. для первого элемента множества, единицы, функция вернет true для значения два, а для второго элемента, двойки — для значения четыре.
Практический пример
Функциональное программирование забавно, но далеко от идеала при использовании PHP. Поэтому я не рекомендую вам писать все приложение подобным способом. Однако теперь, когда вы изучили, как PHP применим к функциональному программированию, вы можете использовать эти знания в ваших повседневных проектах.
Здесь приведен пример модуля аутентификации. Класс AuthPlugin обеспечивает метод, который получает имя пользователя и пароль и волшебным образом производит аутентификацию пользователя и устанавливает права доступа.
class AuthPlugin {
private $permissions = array();
function authenticate($username, $password) {
$this->verifyUser($username, $password);
$adminModules = new AdminModules();
$this->permissions[] = $adminModules->allowRead($username);
$this->permissions[] = $adminModules->allowWrite($username);
$this->permissions[] = $adminModules->allowExecute($username);
}
private function verifyUser($username, $password) {
}
}
Сейчас этот код может показаться вполне нормальным, но в нем есть огромная проблема. 80% метода authenticate() пользуется информацией из AdminModules. А это создает очень сильную зависимость.
Было бы намного разумнее взять эти три вызова и создать отдельный метод с использованием AdminModules.
Таким образом, переместив генерирование в AdminModules, нам удалось свести три зависимости всего к одной. Public-интерфейс AdminModules также был упрощен с трех до одного метода. Однако мы еще не у цели — плагин AuthPlugin до сих пор напрямую зависит от класса AdminModules.
Объектно-ориентированный подход
Если мы хотим, чтобы наш плагин аутентификации мог быть использован в любом модуле, нам нужно определить общий интерфейс для этих модулей. Давайте введем зависимость и определим интерфейс.
class AuthPlugin {
private $permissions = array();
private $appModule;
function __construct(ApplicationModule $appModule) {
$this->appModule = $appModule;
}
function authenticate($username, $password) {
$this->verifyUser($username, $password);
$this->permissions = array_merge(
$this->permissions,
$this->appModule->getPermissions($username)
);
}
private function verifyUser($username, $password) {
}
}
AuthPlugin получил конструктор. Конструктор принимает параметр типа ApplicationModule, интерфейс, и вызывает функцию getPermissions() для этого внедренного объекта.
interface ApplicationModule {
public function getPermissions($username);
}
Интерфейс ApplicationModule определяет единственный public-метод getPermissions(), с именем пользователя в качестве параметра.
class AdminModules implements ApplicationModule {
}
И наконец, в классе AdminModules необходимо реализовать интерфейс ApplicationModule.
Теперь стало намного лучше. Наш плагин AuthPlugin зависит только от интерфейса. AdminModules зависит от того же самого интерфейса, поэтому AuthPlugin стал независимым от модуля. Мы можем создавать любое количество модулей, каждый из которых реализует интерфейс ApplicationModule, и плагин AuthPlugin будет способен работать с ними со всеми.
Функциональный подход
Другой способ избавиться от зависимости и сделать возможным использование плагина AuthPlugin модулем AdminModule, или любыми другими модулями, это внедрение в эти модули зависимости от AuthPlugin. AuthPlugin будет обеспечивать возможность определять функцию аутентификации, и каждое приложение будет представлено своей собственной функцией getPermission().
class AdminModules {
private $authPlugin;
function __construct(Authentitcation $authPlugin) {
$this->authPlugin = $authPlugin;
}
private function allowRead($username) {
return "yes";
}
private function allowWrite($username) {
return "no";
}
private function allowExecute($username) {
return $username == "joe" ? "yes" : "no";
}
private function authenticate() {
$this->authPlugin->setPermissions(
function($username) {
$permissions = array();
$permissions[] = $this->allowRead($username);
$permissions[] = $this->allowWrite($username);
$permissions[] = $this->allowExecute($username);
return $permissions;
}
);
$this->authPlugin->authenticate();
}
}
Начнем с класса AdminModule. Он больше не реализует ничего. Однако, в нем используется внедренный объект, который должен реализовывать интерфейс Authentication. В классе AdminModule будет метод authenticate(), который вызывает setPermissions() для AuthPlugin и передает в качестве параметра функцию, которую необходимо задействовать.
interface Authentication {
function setPermissions($permissionGrantingFunction);
function authenticate();
}
Интерфейс Authentication просто определяет два метода.
class AuthPlugin implements Authentication {
private $permissions = array();
private $appModule;
private $permissionsFunction;
function __construct(ApplicationModule $appModule) {
$this->appModule = $appModule;
}
function authenticate($username, $password) {
$this->verifyUser($username, $password);
$this->permissions = $this->permissionsFunction($username);
}
private function verifyUser($username, $password) {
}
public function setPermissions($permissionGrantingFunction) {
$this->permissionsFunction = $permissionGrantingFunction;
}
}
И наконец, AuthPlugin реализует Authentication и определяет входную функцию в private атрибуте класса. Теперь функция authentication() стала туповатой. Она просто вызывает функцию и затем устанавливает возвращаемое значение. Она полностью изолирована от входных объектов.
Если мы посмотрим на схему, то увидим два важных изменения:
- Вместо AdminModule, AuthPlugin реализует интерфейс;
- AuthPlugin передает с помощью обратного вызова модуль AdminModule или любой другой модуль, переданный в функцию permissionsFunction.
Какой подход использовать?
Нет однозначно правильного ответа на этот вопрос. Я бы сказал, что если процесс определения прав доступа в достаточной мере зависит от прикладного модуля, то объектно-ориентированный подход является более подходящим.
Однако если вы думаете, что каждый модуль приложения должен обеспечивать функцию аутентификации, и ваш плагин AuthPlugin это просто базис, обеспечивающий функциональность аутентификации и не имеющий никакой информации о пользователях и процедурах, тогда вам по пути с функциональным подходом.
Функциональный подход делает ваш плагин AuthPlugin очень абстрактным, и вы можете не бояться зависеть от него. Однако если вы планируете наделить ваш AuthPlugin большими способностями и информацией о пользователе и вашей системе, тогда он станет слишком конкретным, и вы не захотите зависеть от него. В этом случае, выбирайте объектно-ориентированный подход и позвольте конкретному AuthPlugin зависеть от более абстрактных прикладных модулей.
Перевод статьи «Functional Programming in PHP» был подготовлен дружной командой проекта Сайтостроение от А до Я.