Immutability and beyond: verifying program behaviour with Psalm

As a language, PHP permits you to do basically anything. There are some built-in runtime constraints (like parameter and return types) that you can opt into, but it’s otherwise pretty free-wheeling. Fans of paradigms that work well in other languages (such as functional programming) can find themselves a little adrift in PHP land.

Psalm — a type-checking static analysis tool — adds some strictness to the everyday task of writing PHP. Its strictness is customisable: you can choose to have Psalm complain about only the most basic things (like undefined classes and undefined variables), but you can also choose to have Psalm complain about every type-related issue it can discover. Some might consider the latter option unnecessary masochism, but many embrace the challenge of writing code that passes Psalm’s most rigid checks.

I've recently added a raft of annotations that can help functional programming fans better describe their program’s behaviour, along with corresponding Psalm errors when code is deemed to have fallen foul of those annotations.

Why functional programming?

There are already plenty of ways to think about how you structure your code (one example: cyclomatic complexity). Functional programming, with its focus on immutability and functional purity, gives you yet another way to rate your code’s architecture. It’s one that many find helpful when figuring out how to improve their code-writing skills.

If you're only dimly aware of what functional programming is, and how it can be useful in an object-oriented language like PHP, this is a good introductory video.

This article isn't designed to get everyone using these patterns, but I hope it'll pique your interest.

Functional programming fundamentals

Immutability

An immutable object is one in which every property is immutable — once instantiated, there’s no way for its state to change.

Immutable objects are easier to reason about than regular objects, as any given object’s properties will always have the same values, and their methods will always behave in a deterministic fashion.

One can easily create an immutable object whose properties are impossible to change at runtime:

<?php final class CantTouchThis { private string $a; private int $b; public function __construct(string $a, int $b) { $this->a = $a; $this->b = $b; } public function getA() : string { return $this->a; } public function getB() : int { return $this->b; } }

Purity

The idea of immutability is closely linked to the concept of pure functions.

A pure function is one whose output is completely deterministic given its input, and which has no side effects. We can make an addition function whose purity is trivial to determine:

<?php class Arithmetic { public static function add(int $left, int $right) : int { return $left + $right; } } echo Arithmetic::add(40, 2); echo Arithmetic::add(40, 2); // same value is emitted

Some operations render a function impure:

<?php class Arithmetic { public static function addCumulative(int $left) : int { /** @var int */ static $i = 0; // side-effect $i += $left; return $i; } } echo Arithmetic::addCumulative(3); // outputs 3 echo Arithmetic::addCumulative(3); // outputs 6

We can see that the output of Arithmetic::addCumulative is not solely dependent on its input.

Fans of functional programming generally prefer to put as much logic into pure functions as possible, leaving a few impure functions to do messy work (database access, writing to files, and so on). Pure functions are also very easy to test.

A quote from a former colleague and fan of functional programming:

Immutability is nice because it’s a guarantee that the type signature fully encapsulates the behaviour of your function. Humans are good at reasoning about the local behaviour of functions, but type checkers are much better at reasoning about their global behaviour.

Annotating immutability in Psalm

This section introduces a bunch of new Psalm annotations:

@psalm-readonly (for properties)

(for properties) @psalm-pure , @psalm-mutation-free and @psalm-external-mutation-free (for functions and methods)

, and (for functions and methods) @psalm-immutable (for classes)

Per-property immutability

Some object-oriented languages have built-in support for public properties that can be read anywhere, but only written to once (on initialisation). PHP doesn't support the syntax directly, but developers can simulate the idea by making the property private and adding a public get<PropertyName> method:

<?php class A { private string $s; public function __construct(string $s) { $this->s = $s; } public function getS() : string { return $this->s; } }

This works well, but can be unnecessary boilerplate — especially if the only consumers of A are internal to your codebase.

Psalm now supports an annotation for properties, @psalm-readonly — suggested by Nuno Maduro — that tells Psalm to prohibit mutation of the property:

<?php class B { /** @psalm-readonly */ public string $s; public function __construct(string $s) { $this->s = $s; } } $b = new B("hello"); echo $b->s; $b->s = "boo"; // disallowed

Depending on how your app is built, using a @psalm-readonly property may be preferable to a private property and public getter.

Mutation-free methods

Many instance methods don’t change state — like the explicit getters seen above, their output is just a function of their instance’s properties. Those methods are not wholly pure, but pure-ish.

Psalm supports an annotation that ensures that behaviour — @psalm-mutation-free :

<?php class D { private string $s; public function __construct(string $s) { $this->s = $s; } /** * @psalm-mutation-free */ public function getShort() : string { return substr($this->s, 0, 5); } /** * @psalm-mutation-free */ public function getShortMutating() : string { $this->s .= "hello"; return substr($this->s, 0, 5); } /** * Psalm knows that simple property-getting methods * are mutation-free */ public function getS() : string { return $this->s; } } $d = new D("hello"); echo $d->getShort(); // this is fine $d->getShort(); // this is unused

Calling any mutation-free method has no side effects.

Annotating immutable classes in Psalm

You can use the annotation @psalm-immutable on any class you like — it’s the equivalent of adding @psalm-readonly to every property and @psalm-mutation-free to every instance method of that class.

Psalm verifies that you’re using a @psalm-immutable class as intended, preventing any side effects.

We can annotate the class above to declare it all-around immutable, enabling us to make the property publicly visible and remove the boilerplate getter.

<?php /** @psalm-immutable */ class E { public string $s; public function __construct(string $s) { $this->s = $s; } public function getShort() : string { return substr($this->s, 0, 5); } } $e = new E("hello"); echo $e->getShort(); // this is fine $e->getShort(); // this is unused $e->s = "bad"; // this is an error

Annotating pure functions

We use @psalm-pure to annotate functions (static methods or regular functions) whose output is wholly dependent on parameter values.

When added to the examples given earlier, we can see Psalm verify and disprove the purity of the respective methods.

<?php class Arithmetic { /** @psalm-pure */ public static function add(int $left, int $right) : int { return $left + $right; } /** @psalm-pure */ public static function addCumulative(int $left) : int { /** @var int */ static $i = 0; // side-effect $i += $left; return $i; } }

Real-world example

We can use immutable classes and pure functions to create a very simple currency implementation.

There are also examples of immutable classes in PHP’s standard library, including DateTimeImmutable:

<?php function foo(DateTimeImmutable $dt) : void { $dt->modify("+1 day")->format("Y-m-d"); // bad echo $dt->modify("+1 day")->format("Y-m-d"); // good }

Going deeper with external-mutation-free

Sometimes you'll have a class with some internal state that can get updated by one of the class’s methods:

<?php class Counter { private int $count = 0; public function increment() : void { $this->count++; } public function getCount() : int { return $this->count; } }

You might think, “Well, that class can’t be used in a pure function — calling Counter::increment clearly has side effects.”

But not so fast! If the instance of Counter is created inside a pure function, calling Counter::increment will not have any side effects outside that function. We can describe this idea with a different annotation on the class — @psalm-external-mutation-free :

<?php /** @psalm-external-mutation-free */ class Counter { private int $count = 0; public function increment() : void { $this->count++; } public function getCount() : int { return $this->count; } } /** @psalm-pure */ function makeCounter() : Counter { $a = new Counter(); $a->increment(); // this is fine return $a; } /** @psalm-pure */ function takeCounter(Counter $a) : Counter { $a->increment(); // Counter already exists, this is a mutation return new Counter(); }

The purity scale

We now have a range of different annotations we can use to annotate instance methods, static methods, and regular functions.

@psalm-pure

the function does not mutate any state external to the function; the function’s output is entirely deterministic based on its parameter input

the function does not mutate any state external to the function; the function’s output is entirely deterministic based on its parameter input @psalm-mutation-free

the function does not mutate any state external to the function, and its output is deterministic based on its parameter input and the attached instance’s properties

the function does not mutate any state external to the function, and its output is deterministic based on its parameter input and the attached instance’s properties @psalm-external-mutation-free

the function does not mutate any state external to the class, and its output is deterministic based on its parameter input and the attached instance’s properties

the function does not mutate any state external to the class, and its output is deterministic based on its parameter input and the attached instance’s properties <no annotation>

functions can do whatever they like

Additionally we have a couple of annotations we can use to annotate classes.