Darkwood Blog Blog
  • Artikel
  • Beobachtung
  • Releases
  • Creators
de
  • en
  • fr
Anmeldung
  • Blog
  • Artikel
  • Beobachtung
  • Releases
  • Creators

⚡ Tools zur Optimierung von PHP: Partielle Funktionsanwendung, Tokens und Ablauf

vom 13. September 2026

Anmelden um auf diesen Beitrag zu reagieren

🚀 1

PHP 8.6 ist zum Zeitpunkt des Verfassens dieses Artikels noch keine stabile Produktionsversion. Die Experimente liefen auf 8.6.0beta2, isoliert von der Host-CLI PHP 8.5.4, die nicht einmal foo(?) parsen kann. Betrachten Sie jede Aussage als Verhalten einer Vorabversion, das ich tatsächlich getestet habe, und nicht als Versprechen für November.

Die erste Frage, die ich stellte, war immer noch diese:

Was passiert mit Flow, wenn PHP selbst besser darin wird, funktionale Pipelines auszudrücken?

Es ist nun das erste Experiment innerhalb eines größeren:

Wie viele Werkzeuge kann modernes PHP bereitstellen, bevor wir eine weitere Abstraktion benötigen?

Drei unabhängige Durchläufe kamen zu derselben Regel.

Benötigt man Argumentbindung? →  PFA
Benötigt man eine schlanke Quellstruktur?  →  PHP-Token
Müssen lokale Dateien durchlaufen werden? →  foreach
Benötigt man tatsächliche Orchestrierung? →  dann sollte man Flow in Betracht ziehen

Verwende zunächst die PHP-Primitive. Füge Abstraktion und Orchestrierung erst dann hinzu, wenn sie ihren Platz verdient haben.

1. Wie wenig Werkzeuge brauchen wir eigentlich?

Darkwood Flow war ein guter Ort, um einen Artikel über Sprachfeatures unterzubringen, aber ein schlechter Ort, um dort aufzuhören. Sobald die partielle Funktionsanwendung ein Closure erzeugte, das Flow bereits akzeptierte, war die interessante Frage nicht mehr: „Was wurde sonst noch in 8.6 veröffentlicht?“ sondern die Frage, ob sich dieselbe Gewohnheit – nach einem Helper, einem Paket, einem Framework zu greifen – eine Ebene tiefer zeigte, nämlich in den Tools, die wir über PHP schreiben.

Dieser Beitrag ist keine Behauptung, dass PHP schneller ist als Rust oder Go. Er ist keine Behauptung, dass Tokens ASTs übertreffen. Es sind drei Messungen einer einfacheren Frage: Hat sich die nächste Abstraktion ihren Platz verdient?

2. PHP 8.6 entfernt den Closure-Adapter

Der Preis ist nicht „du hast eine Funktion geschrieben“. Der Aufwand besteht in der zusätzlichen Funktion, die man nur schreibt, damit eine andere Funktion ein übrig gebliebenes Argument sehen kann.

fn ($value) => transform($value, $configuration)

Die partielle Funktionsanwendung in PHP 8.6 ist die native Primitive:

transform(?, $configuration)

Das offizielle Flow-Beispiel examples/flow.php hat diesen Aufwand nicht. Diese Aufgaben sind echte Funktionskörper. Die Adapter befinden sich im Verbraucher-Code. Die LanguagePipelineFactory von nolife-language schließt über einen Korpus. Sobald dieser Körper eine benannte Funktion ist – und nur dann –, ist die verbleibende Closure ein Adapter:

static fn (BenchmarkState $value): BenchmarkState => loadPassages($value, $corpus);

Der Budget-Schritt von flow-pipe ist die kleinere Variante: fn ($value) => applyBudget($value, $budget).

Nicht jeder Wrapper ist so aufwendig. flow-pipe schreibt auch fn ($ctx) => $step->apply($ctx). Diese Methode ist bereits unär. Die First-Class-Callables von PHP 8.1 machen sie bereits überflüssig: $step->apply(...).

PFA setzt dort an, wo FCC aufhört: Ein oder mehrere Argumente sind bereits bekannt. Ein Aufruf, der ? oder ... enthält, wird nicht ausgeführt. Er gibt ein Closure zurück.

$load = loadPassages(?, $corpus);
$trim = applyBudget(?, 8);

Reflexion zu 8.6.0beta2:

load PFA: Closure static (BenchmarkState $state): BenchmarkState
trim PFA: Closure static (string $text): string

Das sind dieselben Formen wie bei den Pfeil-Adaptern, ohne die Typen neu zu deklarieren. foo(...) ist die Syntax für erstklassige Aufrufobjekte in PHP 8.1 – nun der degenerierte Fall derselben Funktion.

Ich würde das nicht als Revolution bezeichnen. Es ist der Adapter, den ihr bereits geschrieben habt, wobei die Typen bei der ursprünglichen Funktion belassen wurden. Arrow und PFA unterschieden sich in 8.6.0beta2 um einige Nanosekunden. Die Treiber-Schleife von Flow dominiert diesen Mikro-Benchmark nach wie vor. Bei PFA geht es um Lesbarkeit, nicht um Leistung.

3. ? und ... sind nicht dasselbe

Das ist der Teil, bei dem man leicht Fehler machen kann – auch wenn man nur den Haupttext des v2-RFC liest und es dabei belässt. Eine mit 8.6 eingeführte Änderung: Jedes ? ist in der resultierenden Closure erforderlich, selbst wenn der ursprüngliche Parameter einen Standardwert hatte. ... behält weiterhin seine Optionalität bei.

$question = exampleOptional(?, ?);
// erforderliche Arität: 2. $c erhält immer seinen Standardwert.

$ellipsis = exampleOptional('foo', ...);
// erforderliche Arität: 0. $ellipsis() verwendet beide Standardwerte.

Benannte Platzhalter ordnen die Closure neu an, nicht den zugrunde liegenden Aufruf. Zwei Einschränkungen verändern die Art und Weise, wie Sie Pipelines schreiben.

Gebundene Argumente werden zum Zeitpunkt der Erstellung ausgeführt, nicht beim Aufruf. Eine Pfeilfunktion verzögert den inneren Aufruf; PFA tut dies nicht.

new kann nicht partiell verwendet werden. new stdClass(?) führt zu einem Fehler mit der Meldung Cannot create Closure for new expression. Statische Factory-Methoden sind kein Problem.

Die Callback-Falle ist die, die im RFC bereits beschrieben wurde:

intval(?)('10', 2);   // 10  — zusätzliches Argument wird ignoriert
intval(...)('10', 2); // 2   — 2 wurde zu $base

array_find() und ähnliche Funktionen übergeben einen Schlüssel als zweites Argument. Verwenden Sie vorzugsweise intval(?), es sei denn, Sie möchten den Rest weiterleiten.

4. PFA in Flow integrieren

Ich habe Flow nicht gepatcht. Ich habe die Partials einfach übergeben.

Zuvor war die verbleibende Steuer noch ein Pfeil:

$flow = new Flow(
    static fn (BenchmarkState $state): BenchmarkState => loadPassages($state, $corpus),
    driver: new FiberDriver(),
);
$flow->fn(
    static fn (BenchmarkState $state): BenchmarkState => applyBudgetToState($state, 12),
);

Danach:

$flow = new Flow(loadPassages(?, $corpus), driver: new FiberDriver());
$flow
    ->fn(applyBudgetToState(?, 12))
    ->fn(collect(?, $box));

$flow(new Ip(new BenchmarkState()));
$flow->await();

Das ist der Code in examples/C-flow-pfa.php. Das Ergebnis unter 8.6.0beta2 war der geladene, budgetierte BenchmarkState. collect(?, $box) ist das FlowCollector-Muster, das in eine Funktion extrahiert wurde. PFA hat keinen Collector erfunden; es hat lediglich den Adapter um einen solchen entfernt.

Mit Flow ist nichts passiert. Das ist das interessante Ergebnis. PFA gibt bereits das Closure zurück, das Flows fn() erwartet. Kein partial()-Helper. Keine Curry-API. Kein Grund, die php: >=8.5-Untergrenze von Flow anzuheben. Die Erweiterung von Closure zu einem generischen callable ist nach wie vor eine schlechte Idee: PHP-Callable-Arrays kollidieren mit der bestehenden Array-/Config-Semantik von Flow.

PHP ist für die Syntax und die Argumentbindung zuständig. Flow ist für die Ausführung zuständig.

5. Pipe ist keine partielle Anwendung

Der Pipe-Operator wurde in Version 8.5 eingeführt. Seine rechte Seite muss ein Callable mit einem einzigen Parameter sein. Deshalb waren die Pipelines in 8.5 voller Pfeile in Klammern. PFA ist die fehlende Hälfte dieses Satzes, kein Ersatz für den Satz.

Gleiche Eingabe, zwei Laufzeiten. Natives PHP:

$input
    |> removeNoise(...)
    |> normalizeWhitespace(...)
    |> applyBudget(?, 14);

Flow: dieselben drei Aufrufbaren plus collect(?, $box), FiberDriver, await(). Beide erzeugten 'hello world'.

PFA  → Argumente binden
|>   → einen Wert einbinden
Flow → Jobs ausführen

Sie überschneiden sich bei der Komposition unärer Funktionen. Bei der Multi-IP-Planung, bei Treibern, Fehler-Jobs oder Ereignissen gibt es keine Überschneidungen. Flows fn() dient der Komposition, ist kein Pipe-Operator. Die Verwendung von Pipes erfolgt in den Verbrauchern. Wenn Sie diese Schleife nicht benötigen, brauchen Sie Flow für die Kette nicht.

6. Die „Primitive-First“-Regel

Wir haben es vermieden, partial() zu Flow hinzuzufügen, da PHP das Problem bereits löst.

Das ist eine umfassendere technische Regel, als ein Artikel über Sprachfeatures normalerweise zugibt:

Bevorzuge die Sprachprimitive, bevor du die Framework-Abstraktion erfindest.

Welche weiteren Tooling-Abstraktionen können wir vermeiden?

7. Ein Experiment zum Thema „PHP-Speed-Tooling“

Der aussagekräftige Satz lautet nicht: „PHP ist schneller als eine andere Sprache.“ Er lautet vielmehr:

Wähle die einfachste Darstellung, die dennoch die Informationen enthält, die dein Tool benötigt.

Das Auffinden von Pfeilfunktionen, die bereits PFA oder FCC sind, ist ein lexikalisches Problem. PHP stellt bereits einen Tokenizer zur Verfügung. Ein AST würde uns Gültigkeitsbereiche und Typen liefern, die wir nicht benötigen, um zu sagen: „Diese fn ist ein einzelner Aufruf mit einem übrig gebliebenen Argument.“ Ob Token schneller sind als ein AST, ist eine Erwartung. Wir haben keinen Zwilling gemessen.

8. tools/pfa-opportunities

Der konservative PFA Opportunity Scanner sucht nach diesen Pfeilfunktionen.

Darkwood bewahrt kleine, repository-lokale Entwicklungswerkzeuge unter tools/ auf. Das ist eine Konvention dieses Experiments, keine Aussage über PHP im Allgemeinen. Ein kleines Entwicklungsproblem erfordert nicht automatisch ein Paket, ein Framework oder eine separate Anwendung. Manchmal befindet sich die richtige Lösung direkt neben dem Code, den sie analysiert.

tools/
└── pfa-opportunities/
    ├── bin/scan.php
    ├── src/functions.php
    └── fixtures/
PHP-Dateien
    ↓
PhpToken::tokenize()
    ↓
ignorierbare Token entfernen
    ↓
einfache Pfeilfunktionen finden
    ↓
aufrufbare Form klassifizieren
    ↓
PFA / FCC / ignorieren

Das Tool verwendet PhpToken::tokenize(), da isIgnorable() nativ ist und jedes Token bereits seine Zeile kennt. Kein PHP-Parser. Kein Symfony Finder. RecursiveDirectoryIterator reicht aus. Nur Bericht. Etwa 540 Zeilen. Falsch-negative Ergebnisse sind in Ordnung. Ein falsch-positiver PFA-Vorschlag hingegen nicht.

Die Formen mit hoher Konfidenz:

fn ($x) => foo($x, $bound);     // PFA  → foo(?, $bound)
fn ($x) => $step->apply($x);    // FCC  → $step->apply(...)

Die eigene, ehrliche PFA-Stelle des Tools ist eine Funktion mit zwei Argumenten. Der einfache Ausführungscode ist ein foreach:

$config = new ScanConfig();
foreach ($files as $file) {
    foreach (scanFile($file, $config) as $finding) {
 echo formatFinding($finding, displayPath($finding->file, $roots)), "\n";
    }
}

PFA bindet die Konfiguration einmalig: $scan = scanFile(?, $config);.

9. Der Scanner findet fast keine PFA

Bei nolife-language, flow-pipe und Flow src/:

144 Dateien
498.107 Bytes
51.663 signifikante Token

PFA-Kandidaten     0
FCC-Kandidaten     3
Flow src 0

Null ist ein Ergebnis, kein Fehlschlag. Wir haben ein Tool entwickelt, um eine Hypothese zu testen, und das Tool hat ergeben, dass die ausgewählten Produktionsbäume derzeit keine offensichtlichen PFA-Migrationsstellen enthalten. Offizielle Flow-Jobs sind echte Körper. Die Closure use ($corpus) von LanguagePipelineFactory ist ebenfalls ein echter Körper.

Die drei FCC-Treffer bestätigen die erste Untersuchung:

  • TokenPipelineFlowRunner: $first->apply(...), $step->apply(...)
  • CompressChunkStep: $this->applyToChunk(...)

Die beiden PFA-Treffer treten nur auf, wenn man die Playground-Datei scannt, aus der loadPassages und applyBudget absichtlich extrahiert wurden.

10. Token reichen aus – bis sie es nicht mehr tun

Token liefern uns die Syntax. Sie verschaffen uns jedoch nicht auf magische Weise ein semantisches Verständnis.

Der Scanner lehnt Konstrukte ab, deren Sicherheit er nicht nachweisen kann. fn ($p) => $p->toArray() sieht wie FCC aus, bis man bemerkt, dass der Empfänger tatsächlich der Parameter ist. An der Stelle von array_map gibt es kein $p, auf das man $p->toArray(...) schreiben könnte. Dort blind PFA oder FCC vorzuschlagen, wäre eine Täuschung.

Ebenfalls nicht erkannt:

  • Eigenschaftsprädikate (fn ($result) => $result->regression)
  • Boolesche Ausdrücke (fn ($value) => complicated($value) && other($value))
  • verschachtelte Aufrufe (fn ($x) => foo(bar($x), $c))
  • $x, das zweimal verwendet wird oder nicht an erster Stelle steht
  • function-Körper mit mehreren Anweisungen
  • by-ref, variadische Überbleibsel, new, Identitäts-fn ($x) => $x
  • alles, was Typen oder Namensauflösung erfordert, um sicher zu sein

Wenn sich das Problem zu einer zuverlässigen automatisierten Umschreibung entwickelt, könnten ein AST und typbewusste Werkzeuge die richtige Abstraktion sein. „Speed Tooling“ bedeutet, unnötige Mechanismen zu vermeiden, nicht aber, Mechanismen abzulehnen, wenn sie notwendig werden.

11. Sollte das Tool Flow verwenden?

Das gleiche scanFile() kann ein Flow-Job sein, denn genau dafür ist PFA da:

$flow = (new Flow(scanFile(?, $config), driver: new FiberDriver()))
    ->fn(appendScanFindings(?, $bag));

foreach ($files as $file) {
    $flow(new Ip($file));
}
$flow->await();

Beide Runner haben dieselben drei FCC-Standorte gefunden. Acht Iterationen mit 8.6.0beta2:

Einfaches PHP    ~79,5 ms
Flow ~120,2 ms
Spitzen-Speicherbedarf  ~6 MiB bei beiden

Ich werde nicht sagen: „Flow ist langsam.“ Bei einem kleinen, lokalen, CPU-gebundenen Verzeichnisscan fügt Flow Koordinationsaufwand hinzu, ohne ein Koordinationsproblem zu lösen. foreach hat hier die Nase vorn. Noch nicht.

12. Wann Flow seinen Platz verdient

PHP-Primitive unterstützen bereits Argumentbindung, Callables, Pipeline-Syntax, Tokenisierung und einfache Iteration.

Flow wird nützlich, wenn die Ausführung das Problem ist:

mehrere IPs
treibergesteuerte Ausführung
Fehler-Jobs
Ereignisse
IP-Strategien
await()
überlappende Aufgaben, sofern der ausgewählte Treiber dies unterstützt

Dies sind Funktionen, die im Flow-Quellcode verifiziert wurden. Die Bibliothek verfügt über keinen integrierten „branch/join“-Operator, kein Produkt für Wiederholungsversuche und kein Tracing-Produkt, das über Symfony-Ereignisse hinausgeht. Ich werde keine dieser Funktionen vorab vorstellen, als wären sie bereits veröffentlicht worden.

Solange das Durchsuchen noch nicht über das „Durchlaufen dieser Dateien“ hinausgeht, ist foreach die Grundfunktion.

13. PHP-Grundfunktionen zuerst

Das schnellste Werkzeug ist nicht unbedingt dasjenige, das in einer anderen Sprache geschrieben ist oder auf einem hochentwickelten Parser basiert. Manchmal ist es das Werkzeug, das weniger tut.

PFA statt einer Adapter-Abstraktion.
PhpToken statt eines Parser-Stacks.
foreach statt einer Orchestrierungs-Engine.

Und wenn foreach nicht mehr ausreicht, wird Flow interessant.

PHP 8.5 hat uns die Pipe gebracht. PHP 8.6 bringt uns die partielle Anwendung. PHP verfügt zudem schon seit Jahren über einen Tokenizer. Die interessante Frage ist nicht mehr, wie viel Syntax eine Bibliothek bereitstellen kann, sondern wie wenig sie bereitstellen muss, bevor die nächste Primitive tatsächlich versagt.

Zuerst die PHP-Primitive. Füge erst dann eine Tooling-Abstraktion hinzu, wenn das Problem dies erfordert. Füge Flow hinzu, wenn die Ausführung zur Orchestrierung wird.

Demo-Repository

Als Spielwiese dient das Symfony-Studien-Repo flow-partial-function-application. Die Skripte befinden sich in examples/. Der PFA-Opportunity-Scanner befindet sich in tools/pfa-opportunities/. Flow ist eine Pfadabhängigkeit und wurde nicht verändert.

docker compose run --rm php86 php examples/run-all.php
docker compose run --rm php86 php tools/pfa-opportunities/bin/scan.php \
  /work/content/nolife-language/src \
  /work/content/flow-pipe/src \
  /work/darkwood/src/Darkwood/Component/Flow

PHP 8.6.0beta2 (php:8.6-rc-cli). Flow 8.1.x. PHP 8.5 auf dem Host kann die PFA-Beispiele nicht auswerten.

Quellen

  • PHP 8.6: https://www.php.net/archive/2026.php#2026-09-10-1
  • Quellcode: https://github.com/matyo91/flow-partial-function-application
  • Folien: https://github.com/matyo91/slidewire

Anmelden um auf diesen Beitrag zu reagieren

🚀 1

Site

  • Sitemap
  • Kontakt
  • Impressum

Network

  • Hello
  • Blog
  • Apps
  • Photos

Social

Darkwood 2026, alle Rechte vorbehalten