LINQ для PHP. Часть 1. Я его слепила из того, что было, а потом, что было, то и полюбила

в 16:59, , рубрики: functional programming, linq, php, phpstorm, sql, грусть, печаль, похапэ, Программирование, функциональное программирование, метки: , , , , , , , ,

Сказ о том, как PHP на LINQ портировали. Сравнение ныне существующих библиотек с табличками, но без графиков — прилагается.

Картинка код для привлечения внимания (картинок не дождётесь!)

echo Phinq::create($people)
  ->groupBy(function($person) { return $person->residence->region; })
  ->select(function($grouping) {
    $obj = new stdClass();
    $obj->people = $grouping;
    $obj->region = $grouping->getKey();
    return $obj;
  })->orderBy(function($obj) { return $obj->people->count(); }, true)
  ->aggregate(function($current, $next) {
    $count = $next->people->count();
    return $current . sprintf(
      "%d %s (%s) live in the %s regionn",
      $count,
      $count === 1 ? 'person' : 'people',
      $next->people->aggregate(function($current, $next) {
        if ($current !== null) {
          $current .= ', ';
        }
        return $current . sprintf('%s [%s]', $next->name, $next->residence->code);
      }),
      $next->region
    );
  });

Кто видел C# или любой функциональный язык — при виде этого шедевра закатит глаза (если они предварительно не вылетят из орбит). И, наверное, будет прав. Но можно ещё вот так:

$lowNums =
	from('$n')->in($numbers)->
	where('$n < 5')->
	store($digits)->into('digits')->
	select('$digits[$n]');

Только что вы увидели двух зверей из зоопарка библиотек, портирующих LINQ на PHP. LINQ — это вообще-то Language Integrated Query, то есть SQL-подобные запросы, интегрированные в язык. В C# LINQ полагается на синтаксические деревья и имеет две формы записи, но, так как в PHP подобных фенечек ближайшее тысячелетие ждать не приходится, будем считать, что LINQ — это исключительно библиотечка с SQL-подобными методами.

Библиотек, портирующих LINQ на PHP, достаточно много. Ещё бы хоть одна его реально портировала… Но об этом позже, а пока в алфавитном порядке рассмотрим все имеющиеся альтернативы. Чтение предполагается либо последовательное, либо с конца.

LINQ for PHP

Писалось человеком, который готов переделать любой язык в свой любимый (так случилось, что для него — C#), как бы странно его код ни выглядел. Первые сомнения прокрадываются в душу при виде LinqSamples.php:

class Console
{
	public static function WriteLine()
	{
		$args = func_get_args();
		$string = array_shift($args);
		foreach ($args as $i => $value)
		{
			if (is_bool($value))
			{
				$value = $value ? 'True' : 'False';
			}
			$string = str_replace('{'.$i.'}', $value, $string);
		}
		echo $string.'<br />';
	}
...

Вы предположите, что при таком подходе портированный LINQ будет сложно отличить от оригинала? И будете правы. Было (C#):

from n in new int[] { 1, 2, 3 } where n % 2 == 0 select n * 2;

Стало (PHP):

from('$n')->in(array(1,2,3))->where('$n % 2 == 0')->select('$n *2');

Выглядит симпатично.

Лирическое отступление. Почему строчки? Код из них в <моём любимом IDE> не подсвечиваются же! А как же рефакторинг? А как же очепятки? А никак. Подсветки нет, рефакторинга нет, очепятки правят пиром. Синтаксис замыканий (closures) в PHP настолько многословен (сравните x => x + 1 с function ($x) { return $x + 1; }), что практически все разработчики портов LINQ изобретают свои «строчковые лямбды». Где-то можно использовать замыкания, где-то — похапэшные «указатели на функции» (строки 'strlen' и массивы array($object, 'methodName')), где-то — «лямбды», где-то — и то, и другое, и третье. В LINQ for PHP доступны все варианты.

Посмотрим сорцы библиотеки. «Мясо» скрыто в методе LinqForPhp_Objects_Sequence::doIteration: при вызове getIterator() последовательно выполняются все операции, набравшиеся в массиве operations. Операции выполняются последовательно, без цепочной «ленивости», то есть вызов where()->any() приведёт к фильтрации всей последовательности. После первого вызова результат кэшируется.

У автора были позывы добавить комментарии PHPDoc, но надолго его не хватило: документирована в лучшем случае пятая часть кода. Да и синтаксис PHPDoc автор ниасилил — ни у одного @param имя аргумента не указано.

Реализованы стандартные методы: Aggregate, All, Cast, DefaultIfEmpty, Except, FirstOrDefault, GroupBy, OrderBy, Reverse, Single, Take и т.д. Всего порядка 50 штук. Есть конвертация в List, Dictionary — порты дотнетовых коллекций, в которых часть методов по неизвестной причине бросает NotImplementedException, а offsetExists реализован как array_key_exists($index) (вчитайтесь). Ответ на вопрос, на кой икс нужны эти коллекции, оставляю читателям.

Что не было у человека IDE, очень хорошо чувствуется — PhpStorm моментально высвечивает откровенные баги. Тестов в штуках: ноль целых ноль десятых.

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

Phinq

Писалось человеком, который верит в странное светлое будущее. У всех аргументов, принимающих функции, стоит ограничение на тип Closure; у сравнивалок (comparer) — интерфейсы EqualityComparer и т.п.; у коллекций — array. Выглядит это примерно так:

		public function groupJoin(array $collectionToJoinOn, Closure $innerKeySelector, Closure $outerKeySelector, Closure $resultSelector, EqualityComparer $comparer = null)

На первый взгляд логично, но тут вспоминаешь, что… Кроме замыканий есть родные похапэшные «указатели на функции» в виде строк и массивов [объект, имя метода], и они внезапно идут лесом. В интерфейсе EqualityComparer есть только один метод equals (получения хэшей — нет: это забота внутренностей родных похапэшных ассоциативных массивов). И ради каждого сравнения изволь создавать новый класс, функцию не передашь. В groupJoin логично передавать результаты выполнения запросов Phinq, а тип у них совсем не array. И интерфейсы SPL типа Iterator и IteratorAggregate внезапно тоже не являются массивами. Пока остаёшься в рамках примеров с числами и from-where-select — всё хорошо. Как сталкиваешься с суровой реальностью — становится тяжко.

У всех методов обнаруживается документация PHPDoc. Честно написанная самостоятельно для кажого метода. Ну, почти честно — местами чувствуется копипаста, которая приводит к вранью:

/**
 * Correlates elements into groupings of the two collections based on matching keys
 *
 * This is basically an outer join.
 *
 * @param array $collectionToJoinOn
 * @param Closure $innerKeySelector Takes one argument, the element's value, and returns the join key for that object
 * @param Closure $outerKeySelector Takes one argument, the element's value, and returns the join key for that object
 * @param Closure $resultSelector Takes two arguments, the matching elements from each collection, and returns a single value
 * @param EqualityComparer $comparer
 * @return Phinq
 */
public function groupJoin(...

Вы поняли, что делает функция? А что принимает и возвращает resultSelector? Если вы поняли, то вы не читали документирующий комментарий выше, а просто когда-то пользовались GroupJoin в C#.

Реализованы стандартные методы, всего штук 50, но как-то лениво реализованы. Single почему-то выкидывает исключение, если в коллекции единственный элемент — null. У массивов почему-то выбрасывается информация о ключах. Итераторы конвертируются в массивы сразу при создании объекта Phinq (тоже без информации о ключах, разумеется). Единственная возможность получить что-то с ключами на выходе — это конвертировать в коллекцию Dictionary, которая имеет реализацию offsetGet и offsetSet с ценой O(N). Слава богам, в отличие от предыдущей библиотеки, хотя бы List не реализован (метода toList, соответственно, нет).

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

Что же в библиотеке хорошего? Есть тесты. Ура. Поверхностные, конечно. В лучшем случае по паре штук на метод. Безо всяких граничных случаев. Но тесты — есть. Это хорошо.

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

Phinq 2.0

Писалось человеком, который разочаровался в светлом будущем: привет строковым лямбдам, пока ограничениям на аргументы-функции. Хотя EqualityComparer с единственным методом всё ещё с нами. Сверху приправим конвертацией в SQL для построения запросов SQL. Парсинг PHP силами PHP с исключениями вида «Phinq requires the unary boolean operator ('!') to be followed by an expression encased in parentheses». Писать будем долго...

...И, разумеется, не допишем. На переписывание старого Phinq на новый лад автор забил гвоздь. Ветка /branches/2.0 в SVN на далёком сервере — единственное упоминание светлого начинания и печального конца.

Итог: ничего нет, проходим мимо. К использованию не рекомендуется.

PHPLinq

Писалось человеком, который верит, что кому-то нужен ещё один DAL, и который туманно представляет себе возможности оригинального LINQ. В его понимании функции where, orderBy, select, skip, take и остальные можно вызывать в любом порядке, который не имеет значения, потому что порядок их применения задан намертво (и, кстати, нигде не документирован). В его понимании замыкания — это никому не нужная новомодная фенечка. В его понимании single и first — это совершенно одно и то же. В его понимании тесты — это скрипты PHP с вызовом print_r. В его понимании документирующие комментарии — это скопированное название метода и перечисление типов аргументов. В его понимании документация к проекту — это диаграмма классов в формате для VisualStudio.

Я, конечно, могу рассказать про поддержку LINQ to Objects, LINQ to ZendDb, LINQ to Azure, LINQ to MS SQL, LINQ to MySQL, LINQ to SQLite. Но кому нужно всё это разнообразие, если эта библиотека — маловменяемый код, не имеющий отношения ни к функциональному программированию в общем, ни к LINQ в частности, без документации, без тестов?

Итог: эпическое количество LINQ to *, эпическое отсутствие всего остального. К использованию не рекомендуется.

(Внимательный читатель, наверное, уже начинает задаваться вопросом: когда будет что-то рекомендуемое-то? Когда? Ждём, надеемся… верим. И читаем дальше.)

Plinq

Писалось человеком-минималистом, которому очень хотелось иметь LINQ в PHP, но которому было очень лень кодить. Поэтому поддерживается немногим больше 20 методов, среди которых нет ни ThenBy, ни Aggregate. Ленивых вычислений нет вообще. Строчковых «лямбд» нет — только замыкания, только хардкор (учитывая отсутствие ограничений на тип аргументов и текущие баги в PHP, строчковые «указатели на функции» тоже должны работать). Передача замыканий в функции-агрегаторы — обязательно. В большинстве своём методы Plinq — это обёртки над стандартными функциями, плюс конвертация между массивами и итераторами туда-сюда.

Что хорошего в библиотеке? Есть подобие тестов — по паре assert'ов на функцию. Выглядит это примерно так:

function TestOrderBy(&$testArray)
{
    $p = new Plinq($testArray);
    $result = $p->OrderByDescending(function($k, $v){ return $v['int']; });
    assert('key($result) == "key_999"');

    $p = new Plinq($testArray);
    $result = $p->OrderByDescending(function($k, $v){ return $v['date']; });
    assert('key($result) == "key_999"');

    $p = new Plinq($testArray);
    $result = $p->OrderBy(function($k, $v){ return $v['string']; });
    assert('key($result) == "key_0"');
}

(Про $testArray не спрашивайте — приводить здесь не буду. Скажу только, что это такая однострочная фиговина длиной в 138057 символов, которая заставляет мою IDE поскрипывать шестерёнками. Природу массива не изучал.)

Есть что-то, отдалённо напоминающее документирующие комментарии. Из них можно узнать, что функция Diff «Finds different items» (и ведь не скажешь, что соврал).

Итог: полезных ископаемых нет, воды нет, растительности нет, населена… да ничем не населена. К использованию не рекомендуется.

Всё. Кино окончено. Порты LINQ на PHP кончились.

...Ну ладно, ладно, чтобы не завершать на столь печальной ноте, упомянем библиотеку, которая и не порт LINQ вовсе. Но на безрыбье и щука — рыба.

Underscore.php

Писалось человеком, которому очень понравилась Underscore.js (спасибо, ваш К.О.).

Что такое Underscore.js? Это библиотека, которая реализует всякую функциональщину в JavaScript. Вам что-то говорят имена map, filter, reduce, flatten, times? Если нет, то придётся запомнить, что map — это select, reduce — это aggregate, times — это repeat и т.д. В целом сильно напоминает LINQ, только им не является. Ленивых вычислений нет.

Underscore.php — это, соответственно, порт Underscore.js на PHP. Библиотеки настолько родственны, что даже номера версий синхронизированы.

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

Тесты есть: как скопированные из Underscore.js, так и дополнительные.

Качество кода хромает. Статические функции как статические не помечены. Коллбэки вызываются как $f(), то есть про массивы-указатели на функции можно забыть (есть в PHP такая недоработочка). «Лямбдовые» строчки не поддерживаются.

Ваш код будет выглядеть примерно так:

$numbers = __($numbers)->chain()
                       ->select(function($n) { return $n % 2 === 0; })
                       ->reject(function($n) { return $n % 4 === 0; })
                       ->sortBy(function($n) { return -$n; })
                       ->value();

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

Окончательный итог

Табличка:

LINQ для PHP. Часть 1. Я его слепила из того, что было, а потом, что было, то и полюбила

Итог: ждите вторую часть статьи, «Сами с усами».

Баги

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

PHP

  1. Iterator::key() разрешено возвращать только числа и строки.
    1. 45684 A request for foreach to be key-type agnostic
  2. Была фича с укорачиванием синтаксиса замыканий, причём прилагались патчи, анализ и прочее — оформивший фичу разработчик постарался на славу. Но фичу закрыли с результатом «нафиг надо». :-(

PHPStorm IDE

  1. Код PHP внутри строк
    • WI-3477 Inject PHP language inside assert('literal'), eval and similar
    • WI-2377 No autocompletion for php variables inside string with injected language
  2. Анализ PHP кода
    • WI-11110 Undefined method: Undefined method wrongly reported when using closures
  3. Комментарии PHPDoc
    • WI-8270 Error in PhpDoc quick documentation if {link} used twice in a line

Ссылки

P.S. Подскажите, пожалуйста, куда можно запостить аналогичную статью на английском.

Автор: Athari

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js