Lenses in PHP

Lochemem Bruno Michael
5 min readJul 4, 2023

--

bingo-functional

Getters and setters are two commonplace ideas in programming. It is highly likely that you — the reader — have, at some point, encountered the aforestated operations. The sole purpose of a getter is to retrieve an artifact, while that of a setter is to effect a mutation that could be emplacing an artifact in an abstract composite structure — like a class or a list. Please note that the afore-provided descriptions of getters and setters are not exclusive to classes and objects. They are generic and describe abstractions of processes related to the various discretionary logics of retrieval and modification.

To illustrate the aforedescribed concepts are the following elements: a simple list whose contents are usernames and two functions — one with which to extract the first value from the list — and another whose single purpose is adding a name to the front of the list — and thence updating it.

// list of usernames
$usernames = [
'ace411',
'agiroLoki',
'lochbm',
];

// retrieve first name in list
$getFirst = fn (iterable $list) => $list[0];

// update the first name in the list
$setFirst = function (string $name, iterable $list) {
$list[0] = $name;

return $list;
};

$updated = $setFirst('@ace411', $usernames);

var_dump($getFirst($updated)); // "@ace411"

The getter in the snippet above exclusively evaluates to — upon invocation — the first element in a specified list. Much like the getter, the setter subsumes an operation that targets the head of the list (its first element) but conditions its modification by assigning it a new discretionary value. Both functions are pure and demonstrate how to shape data — in this case, the list of usernames.

While it is possible to write many getters and setters in a fashion similar to those displayed in the example above, solely relying on them to mold data is often inadequate. Data often changes. Consider the list from the previous example. It could potentially morph — into, say, aggregates of user properties inclusive of more contact information — or expand into an even bigger assortment of similar data — with the contents of the afore-displayed list as a constituent. Regardless of whether the list above stays the same or undergoes modification, it is prudent to consider using structures that can accommodate the aforedescribed changes — that make it possible to address small system modifications.

The susceptibility of data to change warrants flexible state handling: preferably with an artifact that imbibes targeted retrieval and modification. Enter lenses. The biological analogy here is apt, as lenses are essentially structures that focus — on a specified target — the retrieval mechanisms of getters and modifications ensconced in setters. Lenses confer the ability to write generic getters and setters — and, depending on the implementation, can significantly reduce the requisite boilerplate.

Included in the suite of functions offered by the bingo-functional library are lens primitives — modeled after those in RambdaJS — with which to shape data. Before embarking on a refactoring endeavor—to modify the code from the previous snippet — it is important to install the said library — via Composer.

$ composer require chemem/bingo-functional

Upon successfully installing the library — and the lenses packaged in it — proceed to type the following in a script you can arbitrarily name to modify the first entry in a list of usernames in the code from earlier.

use function Chemem\Bingo\Functional\Functors\Lens\{lens, set};

$usernames = [
'ace411',
'agiroLoki',
'lochbm',
];

$getFirst = fn (iterable $list) => $list[0];

$setFirst = function (string $name, iterable $list) {
$list[0] = $name;

return $list;
};

$updated = set(
// create lens from getter and setter
lens($getFirst, $setFirst),
// new value
'@ace411',
$usernames,
);

var_dump($updated); // ["@ace411", "agiroLoki", "lochbm"]

The code above effects a targeted list update with the lens function — that which essentially creates the lens by subsuming getter and setter in a higher-order function network — and ultimately, in a functor — and the set function that is responsible for updating the focus of the lens. Unlike the example from earlier, the code above explicitly merges getter and setter into a single structure, and thence creates a single focal point: the head of the list. As is the case with the design in Edward Kmett’s pioneering work, the bingo-functional lens API includes primitives with which to make getter and setter boilerplate terse. It is possible to do away with the functions $getFirst and $setFirst as in the example below.

use function Chemem\Bingo\Functional\Functors\Lens\{lensKey, set};

$usernames = [
'ace411',
'agiroLoki',
'lochbm',
];

$updated = set(
// replace original getter and setter
lensKey(0),
// new value
'@ace411',
$usernames,
);

var_dump($updated); // ["@ace411", "agiroLoki", "lochbm"]

Leaner and more compact — no? Creating a lens with lensKey — a function that builds a lens from a specified iterable index (numeric or string), obviates the need for the functions from earlier. The lens presets in bingo-functional like that featured in the example above are well suited to routines that are interoperable with artifacts that constitute the iterable pseudotype. Even with the previous examples that demonstrate a leaner approach to molding data, there is ample room for the assertion of function application with lenses. over is perhaps the primitive that best demonstrates this.

use function Chemem\Bingo\Functional\Functors\Lens\{lensKey, over};

$usernames = [
'ace411',
'agiroLoki',
'lochbm',
];

$updated = over(
lensKey(0),
// function with which to modify lens target
fn (string $username) => \sprintf('@%s', $username),
$usernames,
);

var_dump($updated); // ["@ace411", "agiroLoki", "lochbm"]

The output in the snippet above, though similar to that from the preceding example, is the result of applying discretionary logic to the focus of the lens — via the over operation. If there is an air of familiarity with the over function, then it is likely because of its similarity to the map operation. over does indeed expose a map operation. As mentioned earlier in the text, lenses are fundamentally functors. Functors are objects synonymous with the map operation. map is a conduit for function application. It is a higher-order function that applies — to the item in its jurisdiction — its function input. The said item is, as far as lenses are concerned, the focal point — that created upon instantiation of the lens (by combining getter and setter).

There exists even more room to tinker with lenses — as the API affords those who use it, the ability to compose various combinations of getters and setters to effect modifications of a variety of data. I advise that you peruse the documentation and consider adding lenses to your repertoire.

The series — Functional Programming in PHP — is now a book. The volume eponymous with the blog content is currently available on Leanpub.

Also, the contents of Functional Programming in PHP have been distilled into a course you can find on Educative.

--

--