Transformations

TL;DR

  • Eine transformation formt einen Stream in einen anderen um: zwei Felder in einen Record packen, das dritte Element eines Tupels verwerfen, eine Liste flatten, einen Wert-Stream gegen einen Praedikat-Stream filtern, einen Wert eine gezaehlte Anzahl von Malen wiederholen, zwei Streams in einen faechern.
  • transformations haben den gleichen typisierten Vertrag wie components (positionelle typisierte Inputs, positionelle typisierte Outputs, typisierte pro-vertex Parameter), sodass die Typinferenz des backend den gesamten Graphen weiterhin zur Edit-Zeit validiert.
  • Die Built-ins sind eine inline-eingebaute Graph-Operation — kein Container, kein Image-Build, keine requirements.txt, kein Scheduling, kein Netzwerk-Hop. Die Plattform fuehrt sie inline als Graph-Plumbing aus.
  • Das backend kann eine transformation automatisch einfuegen, wenn ein connect zwei Slots betrifft, die ueberbrueckbar, aber nicht identisch sind. Es fuegt nur transformations ein, nie eine component, und die Antwort listet jede implizite Einfuegung, sodass der Autor inspizieren oder entfernen kann, was gelandet ist.
  • transform ist die eine Ausnahme, die deinen Code ausfuehrt: fuege Python- oder C++-Worker-Quellcode in die vertex-Config ein, und die Plattform baut ihn zur Deploy-Zeit in einen eigenen Container — keine component zu veroeffentlichen, das Image wird ueber den Quellcode-Inhalt gecacht.
  • sketch ist der deploy-blockierende Platzhalter: variadische typisierte I/O plus ein readme-Parameter fuer formfreie Notizen. Der umgebende Graph kann sofort verdrahtet und typgeprueft werden; der Deploy verweigert, bis jeder sketch ersetzt ist.

Warum transformations ihr eigenes Primitiv sind

Eine transformation formt einen Stream in einen anderen um — zwei Felder in einen Record packen, das dritte Element eines Tupels verwerfen, einen Wert eine gezaehlte Anzahl von Malen wiederholen. Die Plattform behandelt sie als inline-eingebaute Graph-Operation, nicht als component, die du schreibst und deployst.

Pipelogic teilt die beiden Belange entlang einer einzigen Linie. Alles, was Code ausfuehrt, laeuft in einem Container. ML-Inferenz, externe API-Aufrufe, Geschaeftsregeln — alles, wo die Arbeit "etwas basierend auf dem Input berechnen" ist — bekommt die volle Container-Behandlung fuer Isolation, Lebenszyklus und Beobachtbarkeit. Alles, was "gleiche Daten, andere Form" ist, ist eine eingebaute transformation. Kein Container, kein Image; die Plattform erkennt die Operation und fuehrt sie inline aus. Der transform-Schritt sitzt bewusst auf der Code-Seite dieser Linie und behaelt dabei die No-Publish-Ergonomie einer transformation: selbst geschriebener Quellcode, zur Deploy-Zeit in einen eigenen Container gebaut.

Der architektonische Grund fuer die Aufteilung ist, dass eine reine Formaenderung keine der Eigenschaften hat, fuer die ein Container existiert. Es gibt keinen Code zu isolieren, keinen Prozess-Lebenszyklus zu verwalten, keine Logs zu zeigen — nur eine deterministische Umformung typisierter Daten. Also inlined die Plattform sie: kein Container zu bauen, kein Image zu pushen, nichts zu schedulen, kein Netzwerk-Hop zwischen vertices. Die Typisierung ist unbetroffen, weil transformations denselben typisierten Vertrag wie components tragen, sodass der Graph end-to-end validiert bleibt.

Mentales Modell — transformation vs. component

   Component                          Transformation
   ┌────────────────────────────┐     ┌────────────────────────────┐
   │ input  (positional, typed) │     │ input  (positional, typed) │
   │ output (positional, typed) │     │ output (positional, typed) │
   │ config (typed)             │     │ config (typed)             │
   │                            │     │                            │
   │ runs in its own container  │     │ runs inline                │
   │ has a container lifecycle  │     │ built-in graph operation   │
   └────────────────────────────┘     └────────────────────────────┘

Die Form des Vertrags ist identisch — dieselben positionellen typisierten Slots, dieselben typisierten Parameter, dieselbe Art, als vertex in einem backend-Graphen aufzutauchen. Der Unterschied ist operativ: eine component wird beim Deploy als Container materialisiert, eine transformation wird in die Laufzeit zwischen ihren benachbarten components gewoben. Aus Sicht des Graphen sind beide vertices; aus Sicht der Laufzeit haben nur components Container-Lebenszyklen.

Siehe Backend-Operationen fuer die Verben, die vertices beider Arten hinzufuegen und verdrahten.

Wann eine transformation das richtige Werkzeug ist

Nutze diesen Rahmen, wann immer der naechste Schritt "diesen typisierten Stream umformen" ist statt "etwas daraus berechnen".

Die haeufigsten Formen, die eine transformation handhabt, grob in der Reihenfolge der Haeufigkeit:

  • Pack und Unpackpack_record kombiniert mehrere typisierte Streams in einen einzigen Record-typisierten Stream; unpack_record macht das Inverse. Dasselbe fuer Tupel (pack_tuple / unpack_tuple), Named Types (pack_named / unpack_named) und Summentypen (pack_union / unpack_union). Das richtige Primitiv, sobald der Vorgaenger mehrere Teile produziert und der Nachfolger sie als ein Composite will (oder umgekehrt).
  • Kardinalitaet umformenflatten macht aus einem Stream von Listen einen Stream von Elementen; lift_unroll teilt einen Listen-typisierten Stream in einen Groessen-Stream plus einen Element-Stream auf (und lift_reroll ist das Inverse, das Elemente wieder zu Listen der gegebenen Groessen sammelt); list_length produziert den count einer eingehenden Liste; repeat liest einen count n auf einem Input und einen Wert auf einem anderen und emittiert diesen Wert n Mal pro Tick. Die richtigen Primitive fuer "ich brauche viele, wo ich eins habe" oder "ich brauche eins, wo ich viele habe".
  • Streams kombinierenjoin ist ein schluesselgematchter Inner Join zweier Streams, der ein Tupel emittiert, wenn beide denselben Schluessel produzieren; merge_streams fuehrt mehrere gleichtypige Streams in Ankunftsreihenfolge zu einem zusammen; select_stream reicht den Input durch, der durch einen numerischen Index gewaehlt wird; shuffle gruppiert Key-Value-Paare nach Schluessel und emittiert jeden Schluessel mit der Liste der Werte, die ihn geteilt haben. Die richtigen Primitive fuer Fan-in-Muster.
  • Typumwandlungconvert_value wandelt einen atomaren Wert tick-fuer-tick in einen anderen atomaren Typ um; constant schleust einen festen konfigurierten Wert in den Graphen. Die richtigen Primitive, wenn die Formen passen, aber der spezifische Typ nicht.
  • Flow Controlfilter paart einen Wert-Stream mit einem Bool-Praedikat-Stream und verwirft die Werte, deren Praedikat-Tick false war; delay_by_one emittiert zuerst einen konfigurierten Initialwert, dann re-emittiert jeder Tick den im vorherigen Tick gesehenen Wert. Die richtigen Primitive zum Ausduennen, Gaten oder Ausrichten von Streams.
  • Selbst geschriebener Codetransform fuehrt Quellcode aus, den du in die vertex-Config einfuegst, zur Deploy-Zeit in einen eigenen Container gebaut; siehe unten.
  • Platzhaltersketch steht fuer einen noch-nicht-gebauten Schritt; siehe unten.

Die obige Gruppierung ist eine Lesehilfe, keine abfragbare Kategorie. Die Live-Menge ist mit ppl component list --query=<name> entdeckbar wie jeder andere Katalogeintrag.

Auto-Insertion macht das connect-Verb klueger

Nutze diesen Rahmen, wenn ppl backend connect zum ersten Mal gelingt und die Antwort vertices erwaehnt, die du nicht hinzugefuegt hast.

Wenn ppl backend connect mit einem vorgelagerten Slot und einem nachgelagerten Slot aufgerufen wird, deren Formen kompatibel-durch-Koerzierung, aber nicht identisch sind — ein Double in einen Slot, der (Double,) erwartet, ein Stream von Werten in einen Slot, der einen Stream von Listen erwartet — kann die Plattform eine ueberbrueckende transformation automatisch einfuegen. Die Antwort enthaelt ein created_vertices- und created_connections-Feld, das jede implizite Hinzufuegung listet.

Die Plattform erfindet keine Verbindungen, die sie nicht typpruefen kann, und sie fuegt nur transformations ein, nie eine component. Die Antwort nach einem connect zu inspizieren ist die Art, zu sehen, was hinzugefuegt wurde; eine ungewollte Auto-Insertion zu entfernen ist das normale ppl backend disconnect plus ppl backend delete-vertex-Paar.

Die Disziplin, die das ermoeglicht, ist, dass backend-Autoren Graphen auf konzeptioneller Ebene verdrahten koennen — "Output 0 von A geht zu Input 0 von B" — ohne jeden Form-Adapter von Hand zu schreiben. Die Plattform handhabt den Boilerplate; der Autor handhabt das Design.

transform — dein Code als Graph-Schritt, keine component noetig

Nutze das, wenn ein Schritt echte Pro-Nachricht-Logik braucht, die Logik aber zu klein, zu lokal oder zu experimentell ist, um eine veroeffentlichte component zu verdienen.

transform nimmt den Quellcode selbst als Konfiguration: source (den Worker-Rumpf, normales Worker-SDK), language (py oder cpp), build_system (dieselben Build-Templates, gegen die veroeffentlichte components kompilieren) und requirements (exakt gepinnte pip-Zeilen, nur Python). Zur Deploy-Zeit kompiliert die Plattform diesen Quellcode auf dem Deployment-Node in ein Container-Image und fuehrt ihn als normalen vertex aus — die einzige transformation, die nicht inlined wird. Nichts wird veroeffentlicht und nichts in eine Registry gepusht; der Code ist Teil des backend-Graphen, versioniert durch das eigene Operation-Log des backend.

Das gebaute Image wird durch den exakten Inhalt des eingefuegten Codes, die Sprache, das Build-Template und die Abhaengigkeitsliste zusammen identifiziert. Ein identischer Schritt verwendet sein Image ueber Deploys hinweg wieder; nur eine echte Aenderung an einem davon baut es neu. Seine variadischen Ports werden durch die umgebende Verdrahtung und explizite constraints gepinnt, sodass der Graph rund um selbst geschriebenen Code vollstaendig typgeprueft bleibt.

Es schliesst ausserdem die Prototyping-Schleife, die mit den interpretierten Expression-components beginnt: skizziere einen Score oder ein Overlay in evaluate_expression oder visualize_expression und klappe die stabilisierte Logik dann an derselben Graph-Position in einen kompilierten transform-Schritt zusammen.

sketch — typisiertes Geruest fuer das, was noch nicht gebaut ist

Nutze das, wenn der umgebende Graph verdrahtet werden muss, bevor ein bestimmter Schritt existiert.

sketch ist die Platzhalter-transformation. Ihre Inputs und Outputs sind variadisch (keine Seite constraint die andere), und ihr einziger Parameter ist ein formfreies readme: String fuer welche Notizen auch immer der Autor dem zukuenftigen Implementierer hinterlassen will. Der Graph bleibt drumherum typpruefbar — die Plattform akzeptiert Verdrahtungen in den und aus dem sketch — sodass der Rest der Arbeit fortschreiten kann, ohne darauf zu warten, dass das fehlende Stueck gebaut wird.

Das Constraint, das das Design ehrlich macht, ist, dass ein backend, das irgendeinen sketch enthaelt, beim Deploy abgelehnt wird. Die Plattform fuehrt keinen Graphen mit Platzhalter-vertices aus; der sketch muss durch eine echte component oder transformation ersetzt werden, bevor das Deployment gelingen kann. Diese Ablehnung ist die tragende Sicherheitseigenschaft — sketches lassen Autoren im Voraus komponieren, ohne je zu einem Deployment zu fuehren, das stillschweigend nichts tut.

Pro-vertex-Konfiguration gilt auf dieselbe Weise

Wo eine transformation einen Parameter nimmt, setzt du ihn via ppl backend change-parameter, identisch zu components: constant nimmt den einzuschleusenden Wert; delay_by_one nimmt den Initialwert, der vor dem ersten verzoegerten Tick emittiert wird. Viele transformations nehmen ueberhaupt keine Parameter — pack_record leitet seine Feldnamen aus dem nachgelagerten Record-Typ ab statt aus einem Parameter, und Operanden wie der count von repeat und das Praedikat von filter kommen auf Input-Streams an, nicht als Konfiguration. Der Parameter-Vertrag, wo vorhanden, ist Teil des Manifests der transformation, typgeprueft beim change-parameter-Aufruf und gegen den Rest des Graphen zur Edit-Zeit validiert.

Diese Einheitlichkeit ist wichtig, weil sie das mentale Modell klein haelt. backend-Autoren brauchen kein zweites Vokabular fuer "transformations konfigurieren vs. components konfigurieren" — es ist dasselbe Vokabular, dieselben Verben, dieselben Operation-Log-Eintraege.

Wo das hineinpasst

transformations sind das Primitiv der Plattform fuer reine Formaenderungen. Sie halten das Typsystem intakt, waehrend sie den Container-Lebenszyklus von Operationen nehmen, die keinen brauchen — es gibt keinen Code zu isolieren oder zu beobachten, also inlined die Plattform sie als Graph-Plumbing, statt einen Container pro vertex zu materialisieren. Der Preis ist ein zusaetzliches Konzept — eine Art von vertex, die keine component ist — und die Rendite ist, dass Form-Adapter inline, eingebaute Graph-Operationen bleiben statt deployter Einheiten.

Die Disziplin, die sie gut nutzt, ist, zuerst zu einer transformation zu greifen, wann immer die Arbeit "gleiche Daten, andere Form" ist, und nur zu einer component zu greifen, wenn die Arbeit echte Berechnung ist. Der Katalog macht die Unterscheidung einfach: wenn ppl component list --query=<keyword> bereits eine transformation zeigt, die das tut, was du brauchst, ist die richtige Antwort, sie einzuverdrahten, nicht eine component um denselben Job zu schreiben.

Verwandt

  • Types — die Formsprache, zwischen der transformations ueberbruecken.
  • Components — das containerisierte Gegenstueck.
  • Backend-Operationen — die Verben, die vertices hinzufuegen und verdrahten.
  • Backends — wo transformations mit components komponiert werden.
  • Solutions — wie die vier Primitive sich zu einem ausgelieferten Produkt komponieren.

War diese Seite hilfreich?