Hello, internals,
I have been thinking about an RFC idea. The subject is pure functions.
First, let us define, for formality, what pure functions are. Straight from
Wikipedia, a pure function is a function that follows the following
properties:
- the function return values are identical for identical arguments.
- the function has no side effects (no mutation of non-local variables,
mutable reference arguments or input/output streams).
In PHP, there is no formal distinction in the language between pure and
impure functions. The proposal would be a new Pure attribute that, for
example, when applied to the function foo, would require it to be pure.
Therefore, the function foo:
- could not call functions and methods that do not have the Pure attribute,
such as those that generate I/O side effects, such as file_get_contents,
file_put_contents, print. - could not access any external values, such as:
- could not access superglobal variables, $_SESSION, $_REQUEST, $_GET,
$_POST etc. - could not access variables from its class - if it is a method - such
as $this->val, self::$val, static::$val. - could not access static variables from other classes, such as
OtherClass::$val. - etc.
- could not access superglobal variables, $_SESSION, $_REQUEST, $_GET,
With the new attribute, an example of a valid pure function would be:
#[Pure]
function my_array_first(array $array, mixed $fallback = null): mixed {
return $array ? $array[array_key_first($array)] : $fallback;
}
echo my_array_first([], 11); // 11
Some examples of invalid pure functions (which would cause Fatal errors)
would be:
#[Pure]
function random_number(int $a): int {
return random_int(10, 20) + $a;
}
echo random_number(10); // Fatal error: pure function random_number() calls
impure function `random_int()`.
class Ha {
private int $a = 1;
#[Pure]
public function getA(): int {
return $this->a;
}
}
new Ha()->getA(); // Fatal error: pure method Ha::getA() accesses $this.
#[Pure]
function request_method(): string {
return $_SERVER['REQUEST_METHOD'];
}
echo request_method(); // Fatal error: pure function request_method()
accesses superglobal $_SERVER.
I believe pure functions could bring benefits in certain areas.
Today, tools such as PHPStan, Psalm and PhpStorm accept annotations such as
@pure, but nobody guarantees that the function respects them. With a native
attribute, that guarantee would become the responsibility of the language
itself. Moreover, we would have the common benefits of pure functions:
- it would be easier to read and test a function, since the result would
depend only on the arguments, without needing mocks for global state,
session or file system - the result of a pure function could be cached (memoization) without
changing the behavior of the program - the compiler and OPcache could, IN THE FUTURE, optimize calls with
constant arguments - it would be safer to execute the code in Fibers or in extensions such as
parallel, since there would be no shared state
However, if you find the idea interesting, the points about the
implementation arise. I am not a great connoisseur of php-src, but I
believe it would be necessary to:
- compile-time validations, to reject the use of $this, superglobals,
static properties etc. inside a pure function - checking the calls. When the target is known at compile time, such as
random_int, the fatal error could be emitted there. But, for dynamic calls,
such as $fn() or $obj->foo() and for functions declared later, the check
would have to happen at runtime. - marking internal functions that are pure as pure.
I would like to hear the list's opinions on this idea. What do you think?
Regards,
Pedro Veloso
Hey Pedro,
On Fri, 25 Sept 2026 at 21:59, Pedro Veloso contatopedrohalves@gmail.com
wrote:
class Ha { private int $a = 1; #[Pure] public function getA(): int { return $this->a; } } new Ha()->getA(); // Fatal error: pure method Ha::getA() accesses $this.
Overall, I would also like to see functional purity in PHP, but a n
instance method is just a function with an implicit additional $this
argument, and it is pure if it causes no state mutation.
Marco Pivetta
Hey Pedro,
On Fri, 25 Sept 2026 at 21:59, Pedro Veloso
contatopedrohalves@gmail.com wrote:class Ha { private int $a = 1; #[Pure] public function getA(): int { return $this->a; } } new Ha()->getA(); // Fatal error: pure method Ha::getA() accesses $this.Overall, I would also like to see functional purity in PHP, but a n
instance method is just a function with an implicit additional$this
argument, and it is pure if it causes no state mutation.Marco Pivetta
I am also a big fan of functional purity in most forms. However, as noted there's already many SA tools that support this. (Via @pure or #[Pure]. There's even an unreleased PER from FIG that has one.). So for this to make sense in the language itself, it needs to offer more benefit than just "the same thing but in the language."
The OP mentioned possible optimizations or inlining or things like that. At least some of those would need to be included initially to justify the RFC. Eg, #[Pure, Memoize] to mark it pure and auto-memoize the function, #[Pure, Inlineable] to allow the compiler/optimizer to compile away the function call, maybe the function call itself could be made cheaper if we know the function is pure... there's likely lots of options, but someone with more engine knowledge would need to say what's feasible. But we would need something.
The other catch is that, as Marco notes, purity is not always trivial to discover. Especially when passing around objects, if you call a method on the object, it MIGHT mutate something? Who knows? So defining precisely what we're able to enforce would be important, too.
--Larry Garfield
The OP mentioned possible optimizations or inlining or things like that. At least some of those would need to be included initially to justify the RFC.
I'm also keen on explicit purity if there is some actual benefit (e.g.
memoisation, inlining, partial evaluation) to making the assertion.
The other catch is that, as Marco notes, purity is not always trivial to discover. Especially when passing around objects, if you call a method on the object, it MIGHT mutate something? Who knows? So defining precisely what we're able to enforce would be important, too.
(As a simple example of not-pure-after-all, strval(1/7) is affected by
the precision ini setting.)
If it is something that is being enforced then it's something that could
potentially be checked during compilation anyway (as soon as an impure
operation is encountered it poisons the function (and, transitively, any
callers to the function)), and so the assertion wouldn't be needed.
Hi Larry,
At least some of those would need to be included initially to justify the RFC.
I’m not sure optimization needs to be the justification here. A
language-enforced purity contract could be useful on its own, and
inlining does not require purity.
PHP already has
zend_try_inline_call(),
which removes eligible calls to user functions that return constants,
without any attribute. It is limited, but expanding inlining does not
depend on adding #[Pure].
Whim (which is partly my experiment for trying these language ideas)
also inlines eligible calls by default. It has two attributes to
influence that decision:
#[Whim\Marker\AlwaysInline]
relaxes some limits; it requests inlining but does not guarantee it.#[Whim\Marker\NeverInline]
prevents inlining.
The inliner checks the function’s
bytecode,
and even allows output
instructions.
Inlining a function that prints something still needs to print it at
the same point in execution.
You can see this example in the
playground,
the “optimized bytecode” tab shows no function calls because Whim
inlines both the pure sum() and the impure greet(), then folds the
arithmetic and string concatenations, without any attributes.
Purity can help other optimizations, such as reusing results or
evaluating calls with constant arguments, depending on the exact
contract. But that is a separate case from inlining.
Enforcing purity can also have a cost. As Pedro notes, known call
targets may allow compile-time checks, while dynamic calls whose
targets we cannot prove need runtime checks. Those checks might be
cheap, and some could be removed, but we should measure that rather
than assume #[Pure] makes calls cheaper.
Personally, I think having the language enforce that promise,
including for dynamic calls, could be a useful benefit even when
static analysis tools support similar annotations. Whether the
restrictions and runtime cost are worth it is a different question. I
just would not make inlining a condition for accepting it, since we
can improve that independently for both pure and impure code.
Cheers,
Seifeddine.