Comments 18
У вас в библиотеке composer.lock закоммичен
Спасибо, удалил.
А почему composer.lock должен быть не закоммичен? Если его не будет, как тогда composer install выполнить?
Вы случаем не путаете его с composer.json?
composer.lock содержит зафиксированные версии пакетов. composer install в первую очередь смотрит на composer.lock файл. Если его нет, тогда решает какие версии пакетов ставить в зависимости от того, что указано в composer.json. composer.lock файл комитят в конечных проектах, чтобы вся команда разработчиков работала с одинаковыми версиями пакетов, но в библиотеках так делать не стоит, чтобы не навязывать конкретные версии пакетов проектам, которые будут пользоваться библиотекой.
А зачем вообще в целом нужны не пустые коллекции, и что будет с не пустой коллекций когда в ней не станет элеметов (clear вызову)?
Типобезопасность - это конечно великолепно что на doc-блоках держится "типа безопасность", но в чем проблема (хотя бы опционально) добавить проверку типа при обновлении коллекции (set, update)?
filterNotNull - если уже делать по аналогии с имеющимися функциями, то логично было бы сделать логику array_filter, когда при вызове без callback все пустые (да не только null) элементы удаляются
Ковариантность - а что простите в доктрине не так? Объявите такой же докблок и норм, нет?
Иммутабельность - старый добрый clone видимо уже не катит? А то что на каждом вызове будет новая коллекция память засирать, это норм?
HashMap - очередной неоправданный велосипед - SplObjectStorage
1) У непустых коллекций отличается сигнатура некоторых методов. Например, возвразаемый тип head и reduce не содержит ни null
, ни Option
т.к. непустая коллекция гарантирует, что хотя бы один элемент в коллекции присутствует. Непустые коллекции позволяют пользоваться такими операциями без каких-либо проверок на null. clear - это по сути тот же filter(fn() => false)
. В статье есть пример, где после операции filter
NonEmpty
префикс коллекции пропадает из-за того, что коллекция может стать пустой.
3) Такое поведение array_filter лично мне кажется не особо явным
4) Ковариантность сама по себе опасна с точки зрения типов. Из-за этого псалм требует иммутабельности класса, чтобы иметь возможность использовать темплейт-параметры в качестве типа передаваемых аргументов методов. Обоснование есть в документации псалма. Доктриновские коллекции мутабельны и в них не получится использовать ковариантность.
5) Увеличенный расход по памяти конечно стоит учитывать при работе с иммутабельными коллекциями. Но в первую очередь библиотека пропагандирует функциональный подход, а иммутабельность - это один из главных столпов.
6) С помощью SplObjectStorage
не получится чейнить операции и там нельзя хранить скалярные типы. Так же, HashMap
используется внутри HashSet
.
Дисклеймер: не, в целом норм, так держать, лучше делать, чем не делать, лови плюсик и вот это всё :)
Но.
5) Увеличенный расход по памяти конечно стоит учитывать при работе с иммутабельными коллекциями. Но в первую очередь библиотека пропагандирует функциональный подход, а иммутабельность - это один из главных столпов.
$a = [];
for ($i=0; $i<1_000_000; $i++){
$a[] = [$i, "$i"];
}
var_dump(memory_get_usage());
$map = \Fp\Collections\HashMap::collect($a);
unset($a);
var_dump(memory_get_usage());
$map->map(fn($value, $key) => $value+2)
->map(fn($value, $key) => $value/2)
->map(fn($value, $key) => $value + 1)
->map(fn($value, $key) => $value + 1)
->filter(fn($value, $key) => $value % 2 === 0);
В map
добавил тот-же var_dump(memory_get_usage());
Результат.
int(441961296)
int(802031336)
int(1603587320)
Fatal error: Allowed memory size of 2147483648 bytes exhausted
Рили?
Второй момент, который лично меня сильно огорчил - это документирование и оригинальный подход к порядку параметров.

callable
- нууу, ок. Зайду внутрь, посмотрю, что за callable
тут ожидается.

ээээ... ШТА!? value
вперёд key
?
Для каждой операции над коллекцией написана дока в соответствующем интерфейсе с суффиксом Ops. Например LinkedList -> Seq -> SeqOps.

Там есть короткие примеры использования в формате REPL.
В конкретных реализациях коллекций просто используется ссылка на доку из интерфейса с помощью inheritDoc, чтобы избежать дублирования документации.
Что касается порядка аргументов, то чаще всего в операциях типа map и фильтр используется именно значение. Ключ можно использовать, но это редкий кейс.
Такой порядок аргументов позволяет в вашем примере написать вот так и не указывать вообще ключ как второй аргумент.
$map->map(fn($value) => $value + 2)
->map(fn($value) => $value / 2)
->map(fn($value) => $value + 1)
->map(fn($value) => $value + 1)
->filter(fn($value) => $value % 2 === 0);
то чаще всего в операциях типа map и фильтр используется именно значение.
Для Map-ов обычно используется Entry, у которой можно достать ключ и значение по необходимости, а если используется деструкция, то сохраняется семантика коллекции ключ->значение
Вообще не пойму какого ляда тащить иммутабельные коллекции в язык, который не является функциональным по своей природе, в котором редко кто пишет многопоточные приложения и который память жрет как не в себя
Вот не надо на счёт памяти, с этим в пхп всё неплохо, но на счёт иммутабельных коллекций в языке со временем жизни рантайма в один реквест (демоны не рассматриваем) я с вами согласен.
Интересный проект. Надо будет оценить производительность. Я в свое время сравнивал nikic/iter (построенной на генераторах) с cakephp/collection (построенной на итераторах) и результат был в пользу последней.
Проект, в целом, показался мне сильно продвинутой функциональной версией того, что не хватает в cakephp/collection.
Дженерик коллекции в PHP