# Knapsack: Clojure-style lazy sequences for PHP, with a functional API as well

> Knapsack is a PHP collection library that implements most of the sequence operations proposed by Clojure, and its structure is unusual in a useful way: the Collection class holds almost no logic of its own, because every method delegates to a standalone function of the same name in a separate file, so the same operation is available as a method and as a plain function you can pass around. The collections are immutable, and most operations return lazy ones.

**DusanKasan/Knapsack** — Collection pipeline library for PHP

- Repository: https://github.com/DusanKasan/Knapsack
- Website: http://dusankasan.github.io/Knapsack/
- Stars: 533 · Forks: 60
- Language: PHP
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/dusankasan-knapsack

## What the library is, and the split that defines it

Knapsack is a collection library for PHP 5.6 and later that implements most of the sequence operations proposed by Clojure sequences, plus some additional ones. All its features are available as functions for functional programming and as a collection pipeline object method.

The primary language is PHP, the licence is MIT, the default branch is master, and the homepage is dusankasan.github.io/Knapsack. The recent releases are 10.0.0 for additional runtime checks and documentation updates from 2017-04-25, 9.0.0 for huge performance improvements from 2017-04-14, and 8.4.0 which added transpose from 2017-04-03. The last push to the repository was on 2026-08-08 with the project not archived, and the repository publishes no GitHub releases, so the versions live in the changelog and in the Composer package rather than in tags.

That release history is worth reading as a whole. Three releases in three weeks in April 2017, one of them a major version with a performance rewrite in it, and then a long tail. The project is not abandoned, since the last push is recent, but the public version history stopped in 2017.

The design decision that defines the library is stated in the README. The heart of Knapsack is its Collection class, however its every method calls a simple function with the same name that does the actual heavy lifting. These are located in the DusanKasan\Knapsack namespace, in a file called collection_functions.php.

So the class is a thin dispatcher and the functions hold the behaviour. That is unusual, and it buys two things. The functions are usable on their own, on plain arrays, without constructing an object at all. And the class methods and the functions cannot drift apart, because a method is a call to the function rather than a reimplementation of it.

The Collection class is a Traversable implementor via IteratorAggregate, and it accepts a Traversable object, an array, or even a callable that produces a Traversable object or array as its constructor argument. It provides most of Clojure's sequence functionality plus some extra features, and it is immutable: operations performed on the collection will return a new collection or value instead of modifying the original.

## Laziness, and the operations that break it

Most of the methods of Collection return lazy collections, such as filter and map, but some return non-lazy collections or simple values. The README is precise about which is which, and that precision is the documentation you actually need.

Some methods return non-lazy collections, reverse being the example, and some return simple values, count being the other. For these operations all of the items in the collection must be iterated over and realized. There are also operations, drop being the example, that iterate over some items of the collection but do not affect or return them in the result. This behaviour, as well as laziness, is noted for each of the operations.

So there are three categories, and the difference between them is a real performance characteristic rather than a documentation nicety. A lazy map does no work until you consume the result. A reverse cannot be lazy, because you cannot know the last element without reading all of them. A drop has to look at the elements it discards, so it is partly eager even though the result is lazy.

That last category is the one people get wrong. drop looks like a filter that throws things away, and it is easy to assume it is free. It is not, and the library says so.

The immutability follows from the same design. Because operations return new collections rather than modifying, a pipeline is safe to branch from, which is what makes the functional style work in a language without persistent data structures.

The result is a collection where you can do:

```php
$collection = Collection::from([1, 2]);

$result = $collection
    ->map('multiplyBy2')
    ->reduce(0, 'add');
    
echo $result; //6

//On the same collection
$differentResult = $collection
    ->map('multiplyBy3')
    ->reduce(0, 'add');
    
echo $differentResult; //9
```

Two maps and two reduces over the same collection, giving 6 and 9 respectively, with the original untouched. The functions passed in are plain named functions, multiplyBy2, multiplyBy3 and add, and the reduce takes the initial value first here, before the function.

The first example in the README is the same thing as a method chain:

```php
$result = Collection::from([1, 2])
    ->map(function($v) {return $v*2;})
    ->reduce(function($tmp, $v) {return $tmp+$v;}, 0);
    
echo $result; //6
```

And then the same computation with the standalone functions instead of the object:

```php
$result = reduce(
    map(
        [1, 2], 
        function($v) {return $v*2;}
    ),
    function($tmp, $v) {return $tmp+$v;},
    0
);

echo $result; //6
```

Both produce 6. That comparison is the clearest statement of the library's premise: the pipeline form and the function form are two faces of the same implementation.

## Construction, and the callable case that exists for a reason

The constructor story is short and has one wrinkle that is worth understanding.

```php
use DusanKasan\Knapsack\Collection;

$collection1 = new Collection([1, 2, 3]);
$collection2 = Collection::from([1, 2, 3]); //preferred since you can call methods on its result directly.
```

The static form is preferred since you can call methods on its result directly, which is the difference between $collection2->map(...) and a static call that returns something you then have to reach into.

Then there is the more interesting case, working with arrays, Traversable objects or callables that produce Traversables:

```php
$collection1 = Collection::from([1, 2, 3]);
$collection2 = Collection::from(new ArrayIterator([1, 2, 3]);

//Used because Generator can not be rewound
$collection2 = Collection::from(function() { //must have 0 arguments
    foreach ([1, 2, 3] as $value) {
        yield $value;
    }
});
```

The third form takes a callable that must have zero arguments and yields values. The comment explains why it exists: it is used because a Generator can not be rewound.

That is the whole reason. A lazy collection is a one-shot thing. If you traverse it twice, the second traversal finds nothing, because the underlying generator has already been consumed. So if you want a collection that can be traversed more than once, you need a source that can produce a fresh sequence each time, and a callable does exactly that.

The other case in the example is an ArrayIterator, which is a Traversable that can be rewound and therefore can back a collection that is traversed more than once.

So there is a rule hiding in the API: a collection built from a generator is single-use, and a collection built from an array, an ArrayIterator or a callable is safe to reuse. Nothing in the type signature tells you that, which is why the comment is there.

The other detail in the example is that Collection::from(new ArrayIterator([1, 2, 3]) is written with an unbalanced parenthesis in the README, a small typo in a line that is otherwise the clearest illustration of the three construction forms.

## The operations, and the two behaviours that will surprise you

The usage examples cover the operations, and two of them have behaviours that differ from what a PHP developer would expect.

The first is first, and it has an optional flag. If array or Traversable would be returned from functions that return an item from the collection, it can be converted to Collection using the optional flag, and by default it returns the item as is:

```php
$result = Collection::from([[[1]]])
    ->first(true)
    ->first();
    
var_dump($result); //[1]
```

So first(true) gives you the item wrapped as a collection, and first() gives you the raw item. The example is a nested array, so the first(true) call returns the array [1] as a collection, and the following first() call returns the array itself.

The second is keys, and this one is a deliberate design decision rather than a bug. Keys are not unique by design, and the reason given is that it would harm performance. The consequence is that calling toArray() on such a collection loses items, so you should call values() before you do that:

```php
$result = Collection::from([1, 2])->concat([3,4]);
    
//arrays have unique keys
$result->toArray(); //[3,4]
$result->values()->toArray(); //[1, 2, 3, 4]

//When iterating, you can have multiple keys.
foreach ($result as $key => $item) {
    echo $key . ':' . $item . PHP_EOL;
}

//0:1
//1:2
//0:3
//1:4
```

The example concatenates two collections. The result has keys 0, 1, 0, 1 for the four values, and toArray() collapses the duplicate keys, giving [3,4], whereas values()->toArray() gives [1, 2, 3, 4]. The foreach at the end shows the raw behaviour, with the keys 0:1, 1:2, 0:3, 1:4.

So a Knapsack collection is not a PHP array with extra methods, it is a sequence with positions, and converting it to an array is lossy when the source array had non-unique keys. In Clojure terms this is the difference between a vector and a map, and the library is closer to the former while accepting the latter as input.

There is also an iterate operation, which is the way to build an infinite or generated sequence, and the Fibonacci example shows it:

```php
$result = Collection::iterate([1,1], function($v) {
        return [$v[1], $v[0] + $v[1]]; //[1, 2], [2, 3] ...
    })
    ->map('\DusanKasan\Knapsack\first') //one of the collection functions
    ->take(5);
    
foreach ($result as $item) {
    echo $item . PHP_EOL;
}

//1
//1
//2
//3
//5
```

The seed is [1,1] and the function returns the next pair each time, and take(5) then bounds it. The map is applied to one of the collection functions, first, using its fully qualified name, and the output is the first five Fibonacci numbers, 1, 1, 2, 3, 5.

And there is the CollectionTrait, which lets you use the whole method set on your own classes. The example is an ArrayIterator subclass that uses the trait, and $iterator->size() returns 3. The trait works on any Traversable by default, and in any other class you have to override the getItems() method it provides. The warning is that after calling filter or any other method that returns a collection, the returned type will actually be Collection, not the original Traversable.

## The repository, and how the project is tested

The repository layout shows a conventional PHP library with a strong emphasis on specification-driven testing.

At the top level there is composer.json, composer.lock, a phpunit.xml.dist for the unit test runner, a phpspec.yml for the specification tests, a .scrutinizer.yml and a .sensiolabs.yml for the two static analysis services, a CHANGELOG.md, a CONTRIBUTING.md, a LICENSE.md and a ci/ directory for the continuous integration configuration. The source is in src/ and the tests are in tests/.

The tests/ directory is the part that tells you how the project is developed, because the README points at three subdirectories within it. There are the specs, which are the phpspec specifications, the scenarios, and the performance tests, which the README says you can run on your machine to see the computation time impact of the library, with the output included in the README itself.

So the library is tested at three levels. Specifications describe the behaviour of each operation, scenarios cover usage patterns across multiple operations, and performance tests measure what the lazy and eager distinction actually costs. A library whose main claim is that laziness and immutability are worth having, and whose main risk is that the eager operations are slower than the user expects, is exactly the kind of thing where publishing the performance numbers matters.

The badges at the top of the README show the continuous integration picture: a build status and a code coverage badge from Scrutinizer CI, a quality score badge, and a SensioLabs Insight badge for PHP static analysis. The SensioLabs badge is notable, because that service was discontinued, so a badge pointing at it is a fossil of the tooling conventions of the time.

The documentation is a prettified version of the README hosted at dusankasan.github.io/Knapsack, which is a simple arrangement that has the advantage of never going out of date relative to the README and the disadvantage of not adding anything to it.

The issue policy in the README is short and generous. Feel free to report any issues you find, the author will do his best to fix them as soon as possible, but community pull requests to fix them are more than welcome. Given the version history stopping in 2017 and the last push in 2026, that wording suggests the project is in a maintenance mode where fixes come from outside.

## How to evaluate it, and what to check first

There are four things to consider.

First, whether PHP 5.6 is a constraint or a comfort. The library targets PHP 5.6 and later, which means it predates the typed properties, arrow functions and other modern syntax, and it means it runs on old installations. If you are on PHP 8 with strict types, the library will still work, but you are using a codebase written for a much older era of the language, and you should check how it behaves under modern strict type error settings rather than assume.

Second, the immutability and laziness are not free. Every operation returns a new collection, so a chain of ten transformations creates ten objects, and the lazy ones do not compute until consumed. If your code style is to mutate and reuse a collection, the mental model has to change, and the README's note that each operation's laziness behaviour is documented individually is the thing to read rather than assume.

Third, the duplicate key behaviour is the trap. Keys are not unique by design, for performance reasons, and toArray() silently drops items when the source had non-unique keys. If you are converting a Knapsack collection to an array anywhere near your persistence or serialisation layer, call values() first. A silent data loss in a getter method is the kind of bug that surfaces much later.

Fourth, the maintenance state, and here the dates are what they are. The last push was on 2026-08-08 so the code is being touched, but the public version history stops at 10.0.0 in April 2017, and the README says the documentation is a prettified copy of itself, so there is no separate documentation site to consult for anything newer. Treat the library as stable and finished rather than actively developed, and check the changelog for the version you would install.

What you get is a mature, well-specified implementation of the Clojure sequence vocabulary for PHP, with the unusual and useful property that every operation is available both as a pipeline method and as a standalone function, and with laziness and immutability documented per operation rather than left to be discovered.

## Conclusion

Adopt Knapsack if you want lazy, immutable collections in PHP with the Clojure sequence vocabulary, particularly if you prefer the standalone function form for composing pipelines, since every operation is available both ways and the class is only a dispatcher over the functions. Do not adopt it if your code depends on mutating a collection in place, because every operation returns a new one, and do not let a Knapsack collection reach your serialisation layer without calling values() first, since the deliberate non-unique key behaviour makes toArray() drop items. Before you commit, check how the library behaves under your PHP version's strict type settings, read the per-operation notes on laziness for the operations you will actually use, and look at the changelog for the version you would install, since the public release history stops at 10.0.0 in 2017 even though the repository is still being touched.

## FAQ

### What is Knapsack?

It is a collection library for PHP 5.6 and later that implements most of the sequence operations proposed by Clojure sequences, plus some additional ones. All its features are available as functions for functional programming and as a collection pipeline object method, and it is MIT licensed with the last push to the repository on 2026-08-08.

### How does the Knapsack Collection class work?

The Collection class holds almost no logic, because its every method calls a simple function with the same name that does the heavy lifting, and those functions live in the DusanKasan\Knapsack namespace in a collection_functions.php file. The class is a Traversable implementor via IteratorAggregate and accepts a Traversable, an array, or a callable producing either.

### Which Knapsack operations are lazy and which are not?

Most methods return lazy collections, but some return non-lazy collections such as reverse, and some return simple values such as count, and for those all items must be iterated over and realized. There are also operations such as drop that iterate over some items without affecting or returning them, and the README notes this behaviour and the laziness for each operation individually.

### Why does toArray() lose items on a Knapsack collection?

Because keys are not unique by design, as making them unique would harm performance. Concat of two collections gives keys 0, 1, 0, 1, and toArray() collapses the duplicates, so you should call values() first, which gives the full list in order.

### How do I install and use Knapsack?

Require it with Composer using composer require dusank/knapsack. Then create a collection with the static constructor Collection::from([1, 2, 3]), which is preferred since you can call methods on its result directly, or with new Collection([1, 2, 3]). There is also a CollectionTrait you can use in an existing class to get the whole method set, and on any Traversable by default.

## Sources

- [DusanKasan/Knapsack on GitHub](https://github.com/DusanKasan/Knapsack)
- [License: MIT](https://github.com/DusanKasan/Knapsack/blob/master/LICENSE)
- [Project website](http://dusankasan.github.io/Knapsack/)
- [README](https://github.com/DusanKasan/Knapsack/blob/master/README.md)
- [Releases](https://github.com/DusanKasan/Knapsack/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dusankasan-knapsack
