Skip to content

Instantly share code, notes, and snippets.

@Crell
Last active April 30, 2026 18:06
Show Gist options
  • Select an option

  • Save Crell/84cab7c3bf2b25e81b7f464945f6b678 to your computer and use it in GitHub Desktop.

Select an option

Save Crell/84cab7c3bf2b25e81b7f464945f6b678 to your computer and use it in GitHub Desktop.
Extension functions, alpha draft

Goal

Extension functions allow using an external function on a value as though it were a method. Syntactically, it looks like a method. At runtime, it is just a function call, and the function can only perform public operations on the value.

This has two main use cases:

  1. Scalar methods. Extension functions may be applied to scalar values, which allows arbitrary functions to be presented as though they were methods on a string or array. Unlike previous scalar method proposals, this is unbounded. While core can and should provide many common operations (length, map, filter, explode, implode, etc.), user-space can provide its own additional ones easily.

  2. Transformation logic. When converting an object of type A (say, an incoming DTO) to type B (a validated DTO, or an Entity), there's a question of where that logic should live. It's not really "part" of A or B. Having a separate service for it is quite clunky. Extension functions offer a 3rd alternative, where the logic lives in its own extension function but may be called on type A. Typically, $a->toB(). (This pattern is very common in Kotlin, from which this logic is largely based.)

Extension functions do NOT fulfill an interface for an object. However, extension functions may be applied to an interface.

Random thought: Could extension functions on interfaces provide implementations, and thus be a backdoor way to have method default implementations? Unclear. Possibly not, but it's an interesting theory.

Description

There are a few possible models for how extension functions could work. They generally break down into class-based and function-based. We're still not sure which approach to take.

Function-based would be a bit easier, and more consistent with how extension functions work in similar languages. It would also be possible (though not necessarily wise, still TBD) to use any function as an extension function. However, they would not have autoloading capability.

Class-based would give us autoloading, at the cost of needing to give every extension function two names; one method name and one autoloadable construct.

This decision will impact the syntax used for extension functions.

The key aspect of extension functions is building a use-based lookup table for each file. This will be a manual step as far as the code and engine is concerned, due to PHP's one-file compilation, but in practice it's likely that IDEs will automate it much as they already do for class and function use statements.

The use statement syntax will be something like:

// If function-based
use Name\Space\func [as funky] for SomeType;

// If class-based
use Name\Space\FuncHolder.func [as funky] for SomeType;

Where SomeType can be any legal PHP variable type except resource.

From that, the compiler can compute a lookup table that will be used at runtime to resolve extension functions

Primitive types in the engine are identified by an integer constant, which can be used as a lookup key. Classes are keyed by their class name.

Given the following use declarations:

use Name\Space\funcA for bool;
use Name\Space\funcB for int;
use Other\Space\funcA for int;
use Name\Space\funcC for SomeClass;
use Name\Space\funcD as beep for OtherClass;

The lookup table will be equivalent to:

// 1 and 2 correspond to bool and int, respectively.
$lookup = [
    'funcA' => [
	    1 => pointer to Name\Space\funcA,
	    2 => pointer to Other\Space\funcA,
    ],
    'funcB' => [
	    2 => pointer to Name\Space\funcB,
    ],
    'funcC' => [
	    '\SomeClass' => pointer to Name\Space\funcC,
    ],
    'beep' => [ // Because it's aliased via `as`
	    '\OtherClass' => pointer to Name\Space\funcD,
    ],
];

When an extension function is encountered, the engine will look up the function name in the table and then look up the type of the variable. For primitives, this will essentially always be O(1).

For object types, if the object type itself is not listed then it will be checked again with the object type's ancestors, in turn. This lookup is therefore O(n), where n is the number of ancestors (parents and interfaces) the object has. In practice, n should generally be fairly small, usually under 10, and frequently only 1-3.

This allows for a fairly fast and robust lookup of the extension function to apply. While slower than a method, the difference will likely be negligible in the majority of cases.

If a lookup fails to find an extension function, an Error will be issued.

The extension functions themselves have essentially 3 options for syntax.

namespace Name\Space;

// Any function is an extension function.
// This applies to `bool` values.
function funcA(bool $b, string $foo) {}

// Object based, function-like.
// This applies only to SomeType values, including anything that would pass a normal type check for it.
extension FuncHolder.func(SomeType $s, int $other) {}

// Object based, object-like syntax.

extension FuncHolder for SomeType {
    public function func(int $other) {
	    // $this is implicitly the SomeType it is applied to.
    }
    
    // Which then maybe means we can have private methods,
    // private to the extension function?  Good or bad?
    private function helper() {}
}

namespace Other\Space;

// Function-based, but only specially marked functions.
// This applies to `int` values.
extension function funcA(int $i, string $foo) {}

In any case, an extension function could either be used like any other method, or use its own +> operator, depending on which we find easier to implement and read.

$i = 5;
// We could do either of these.
$i->double();
$i +> double();

$s = 'beep';
$s->length();
$s +> length();

As a possible add-on, if we can determine at compile time what the type of a variable is, we can pre-resolve the extension function using the same table and emit the appropriate opcodes, avoiding any runtime overhead. (Essentially, at runtime it's just foo($subject, $other, $args).)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment