Zum Inhalt

JDL — Cast-System, Effect-Architektur, Funktions-Refinements und Deklarative Runtime

Status: Normativ
Ersetzt in Teilen: - 02-typsystem.md § 3 – Typ-Konversion - 04-qualifier-effekte-konversionen-wire.md § 4 – Effektsystem - 04-qualifier-effekte-konversionen-wire.md § 5 – Konversionssystem - 20-spec-v3-effect-system-archiv.md – Effect-System-Architektur (Archiv) - 28-syntax-exploration-archiv.md – Konversions- und Effekt-Abschnitte

Bei Widerspruch gilt dieses Dokument.


Präambel

Dieses Dokument konsolidiert drei Designentscheidungen die konzeptuell zusammengehören. Sie sind keine isolierten Änderungen – sie sind Ausdruck eines kohärenten Gesamtbildes davon was JDL ist und wie seine Schichten miteinander zusammenwirken.

Die Entscheidungen im Überblick:

  1. Konversionen werden nicht länger als Sonderform von provide behandelt, sondern als gewöhnliches Protocol mit Operator-Bindung – CastTo[T] via ->.
  2. Effekte sind pure Beschreibungen, keine direkten Ausführungen. Effect[R, E, D] ist ein normaler generischer Typ in der Stdlib — kein Compiler-Primitiv. Die gesamte Semantik ist JDL-Code der auf diesem Typ aufbaut. Das deps-Konstrukt auf Funktionsebene entfällt vollständig.
  3. Querschnittsbelange wie Caching, Retry und Transaktionalität gehören ins Meta-Record der Funktion, nicht in ihre Logik.

Deklarative Konstrukte sind zur Laufzeit composable Dictionaries – und das eröffnet Möglichkeiten die weit über das initiale Design hinausgehen.


1. Das Cast-System

1.1 Motivation

Die vierte Form von provideprovide Target from Source – löste ein konzeptuelles Problem nicht sauber: Konversion ist keine Implementierung eines Protokolls für einen Typ, sondern eine gerichtete Beziehung zwischen zwei Typen. Sie gehörte syntaktisch und semantisch nicht in dieselbe Kategorie wie Protocol-Implementierungen oder Service-Bindungen.

Die Lösung ist einfach: Konversion ist ein gewöhnliches Protocol namens CastTo[T], gebunden an den Operator ->. Damit verschwindet die vierte provide-Form vollständig. provide hat ab jetzt genau drei semantisch klar getrennte Formen:

provide Equatable for User        – Protocol-Implementierung
provide User                      – Methoden am Typ
provide UserDb[PostgresHandler]   – Service-Handler-Bindung

Konversion ist die erste Form – CastTo[T] ist ein Protocol wie jedes andere.

1.2 Der Operator -> als Flow-Metapher

-> ist in JDL bereits als Flow-Symbol etabliert: in Funktionssignaturen beschreibt er den Datenfluss von Eingabe zu Ausgabe, im CallGraph beschreibt er die Sequenz von Knoten. Als Cast-Operator setzt er diese Metapher konsequent fort – ein Wert fließt von einem Typ in einen anderen.

Diese Konsistenz ist kein Zufall, sondern ein Designprinzip: ein Symbol sollte überall dieselbe intuitive Bedeutung tragen.

1.3 Drei Modi

Der entscheidende Gedanke: der Modus eines Casts liegt auf der Implementierung, nicht auf dem Aufruf. Der Entwickler der den Cast implementiert entscheidet ob er verlustfrei, verlustbehaftet oder fallibel ist.

// Lossless – verlustfrei, darf implizit angewendet werden
provide CastTo[UserResponse] for User {
    def castTo(self) -> UserResponse = UserResponse {
        id:    self.id
        name:  self.name
        email: self.email
        role:  self.role
    }
}

// Narrow – verlustbehaftet, immer explizit erzwungen
provide CastTo[UserSummary]: Narrow for User {
    def castTo(self) -> UserSummary = UserSummary {
        name: self.name
    }
}

// Try – fallible, Rückgabetyp wird zu Result[T, E]
provide CastTo[UserId]: Try[ConvError] for User {
    def castTo(self) -> Result[UserId, ConvError] =
        if self.id.value.isEmpty() then
            Err(ConvError.InvalidId { reason: "Id ist leer" })
        else
            Ok(self.id)
}

1.4 Anwendung

val user: User = ...

val response: UserResponse = user         // Lossless – implizit
val response  = user -> UserResponse      // Lossless – explizit
val summary   = user -> UserSummary       // Narrow – explizit erzwungen
val id        =? user -> UserId           // Try – Result muss behandelt werden

1.5 Primitive Widening

Primitive Widening – etwa i32 zu i64 – ist eingebaut und braucht kein provide-Block. Der Compiler kennt die verlustfreien Pfade zwischen primitiven Typen. Verengungen brauchen immer einen expliziten Cast.


2. Effect-Architektur

2.1 Das Grundprinzip: Beschreibung vor Ausführung

Ein Effekt ist kein Aufruf, sondern eine Beschreibung eines Aufrufs. Effect[R, E, D] ist ein normaler generischer Typ in der Stdlib — kein Compiler-Primitiv. Der Compiler hat kein Sonderwissen darüber. Die Laziness ist eine Eigenschaft des Typs selbst, nicht des Compilers — analog dazu wie Futures oder Promises in anderen Sprachen funktionieren.

Der Typ trägt drei Dimensionen:

  • R – Result-Typ: was die Berechnung im Erfolgsfall produziert
  • E – Error-Typ: was schiefgehen kann
  • D – Dependencies: welche Services benötigt werden

Das Mnemonic RED ist bewusst gewählt. Effect[R, E, D] verhält sich analog zu Result[R, E] – nur eine Ebene früher:

Result[R, E]      → beschreibt was passiert IST
Effect[R, E, D]   → beschreibt was passieren WIRD

Effect[User, UserDbError, UserDb] ist korrekt. Effect[Result[User, UserDbError], UserDbError, UserDb] ist falsch – der Fehler lebt entweder im E-Kanal oder im R-Kanal, nie in beiden.

2.2 Zwei Funktionsstile – eine Sprache

JDL kennt kein deps-Konstrukt auf Funktionsebene. Stattdessen gibt es zwei natürliche Stile:

Imperativer Stil — normaler Code, Abhängigkeiten als Parameter, sofortige Ausführung:

def fetchUser(db: UserDb, id: UserId) -> Result[User, UserDbError] =
    db.getUserById(id)

Effect-StilEffect[R, E, D] als normaler Rückgabetyp, lazy durch den Typ selbst:

def fetchUser(id: UserId) -> Effect[User, UserDbError, UserDb] =
    UserDb.getUserById(id)

Der Funktionskörper beider Varianten sieht identisch aus. Der Unterschied liegt im Rückgabetyp — im Effect-Stil gibt die Funktion einen Effect-Wert zurück der erst durch explizites Bereitstellen der Dependencies ausgeführt wird. Das ist normale Typsemantik — kein Compiler-Sonderfall, keine do-Notation, keine versteckte Magie. Die gesamte Kompositions-API ist Stdlib-Code der auf Effect[R, E, D] als gewöhnlichem Typ aufbaut.

Das ist konzeptuell analog zu Effect-TS: die Sprache wird nicht erweitert, der Typ wird genutzt.

2.3 JDL ist multiparadigmatisch

JDL kennt keine Schicht die ausschließlich in einem Paradigma geschrieben wird. Der Ausdrucksstil wechselt je nach Kontext – die Sprache bleibt dieselbe.

┌─────────────────────────────────────────────────────┐
│  D / Host                                           │
│  bootstrapped die Jade VM einmalig                  │
│  kein Teil der JDL-Architektur                      │
└──────────────────────┬──────────────────────────────┘
                       │ startet
┌──────────────────────▼──────────────────────────────┐
│  JDL – Deklarativer Stil                            │
│  Effect-Engine, CallGraph, Aktoren, Workflows       │
│  UI-Beschreibungen, verteilte Graphen               │
│  "wie hängt alles zusammen"                         │
└──────────────────────┬──────────────────────────────┘
                       │ interpretiert
┌──────────────────────▼──────────────────────────────┐
│  JDL – Funktionaler Stil                            │
│  Effect[R, E, D] – pure Beschreibungen              │
│  komponierbar, testbar, seiteneffektfrei            │
│  "was soll passieren"                               │
└──────────────────────┬──────────────────────────────┘
                       │ ausgeführt durch
┌──────────────────────▼──────────────────────────────┐
│  JDL – Imperativer Stil                             │
│  direkte Ausführung, Stdlib, Intrinsics             │
│  Abhängigkeiten als Parameter                       │
│  "wie es tatsächlich passiert"                      │
└─────────────────────────────────────────────────────┘

Kommunikation ist strikt nach unten – keine Schicht ruft nach oben. D ist kein Bestandteil dieser Architektur. Es ist der einmalige Bootstrap-Mechanismus.

2.4 Der CallGraph als Effect-Engine

Der CallGraph ist die Interpretationsschicht – der einzige Ort wo Effect-Beschreibungen zu tatsächlichen Ausführungen werden. Die Effect-Engine ist Teil der Stdlib, kein VM-Primitiv.

CallGraph app(req: HttpRequest) -> Result[HttpResponse, AppError] {
    requires: [UserDb, LogService]
    env: {
        UserDb:     PostgresUserDb { pool }
        LogService: ConsoleLogger {}
    }

    handleRequest(req)
}

Außerhalb des CallGraph: pure Beschreibung. Innerhalb: Ausführung.

2.5 Effect-Komposition

Effects sind über =? komponierbar – das ist ihr Kernvorteil gegenüber direkter Ausführung:

def authenticate(req: HttpRequest) -> Effect[AuthToken, Unauthorized, AuthService]
def validate(token: AuthToken, body: str) -> Effect[CreateUserInput, ValidationFailed, []]
def persist(input: CreateUserInput) -> Effect[User, DbError, UserDb]

// Komposition – sieht aus wie normaler Code, ist lazy:
def handleRequest(req: HttpRequest) -> Effect[User, AppError, [AuthService, UserDb]] =
    val token =? authenticate(req)
    val input =? validate(token, req.body)
    val user  =? persist(input)
    Effect.ok(user)

2.6 Debug vs. Release

Debug:    Effect-AST wird zur Laufzeit aufgebaut und interpretiert
          → inspektierbar, visualisierbar, debuggbar

Release:  Effect-AST wird zur Compile-Zeit aufgebaut und als
          optimierter Bytecode eingebettet
          → kein Interpretations-Overhead zur Laufzeit

Phase 1 implementiert den Debug-Interpreter. Release-Optimierung ist Phase 2.


3. Funktions-Refinements

Querschnittsbelange gehören ins Meta-Record – nicht in die Logik. Die Funktion beschreibt was sie tut. Das Refinement beschreibt wie sie sich verhält. Refinements funktionieren auf beiden Funktionsstilen.

3.1 Cache

type CachePolicy: enum = | Always | Ok | Err

type CacheConfig: struct {
    ttl:  Duration
    only: CachePolicy = CachePolicy.Always
}

typefn Cache(config: CacheConfig) = <{ cache: config }>
// Effect-Stil:
def getCachedUser(id: UserId) -> Effect[User, UserError, UserDb] =
    UserDb.getUserById(id)
:> Cache(CacheConfig { ttl: 24.hours, only: Ok })

// Imperativer Stil:
def getCachedUser(db: UserDb, id: UserId) -> Result[User, UserError] =
    db.getUserById(id)
:> Cache(CacheConfig { ttl: 24.hours, only: Ok })

Cache-Key wird automatisch generiert: Funktionssignatur als Namespace, Hash der Parameter als Schlüssel. Parameter müssen Hashable implementieren – der Compiler prüft das statisch.

3.2 Retry

type BackoffStrategy: enum = | Constant | Linear | Exponential

type RetryConfig: struct {
    max:     u32
    backoff: BackoffStrategy = BackoffStrategy.Exponential
    delay:   Duration        = 100.ms
}

typefn Retry(config: RetryConfig) = <{ retry: config }>

Drei Strategien: Constant, Linear, Exponential. Exponentiell ist der vernünftige Default für Netzwerk-Operationen.

3.3 Timeout

type TimeoutConfig: struct {
    after: Duration
}

typefn Timeout(config: TimeoutConfig) = <{ timeout: config }>

Nach Ablauf scheitert die Operation mit einem Timeout-Fehler. Kombiniert mit Retry ergibt sich ein vollständiges Resilience-Pattern ohne eine Zeile Logik zu schreiben.

3.4 Transactional

Transactional macht eine Funktion zur atomaren Einheit: entweder alle Operationen gelingen, oder keine hinterlässt Spuren.

def persistUserAndAudit(db: UserDb, log: AuditLog, user: User) -> Result[User, AppError] =
    val saved =? db.insert(user)
    val _     =? log.write(saved.id)
    Ok(saved)
:> Transactional

3.5 Komposition von Refinements

def robustGetUser(id: UserId) -> Effect[User, UserError, UserDb] =
    UserDb.getUserById(id)
:> Cache(CacheConfig { ttl: 1.hour, only: Ok })
:> Retry(RetryConfig { max: 3 })
:> Timeout(TimeoutConfig { after: 5.seconds })

Reihenfolge der Ausführung: Timeout → Retry → Cache → Funktion. Der Compiler legt die Reihenfolge fest – nicht der Entwickler.


4. Implizite Transaktionalität via =?

Seiteneffektfreie Pipelines sind bereits implizit transaktional:

def handleRequest(auth: AuthService, db: UserDb, req: HttpRequest) -> Result[User, AppError] =
    val token =? authenticate(auth, req)
    val input =? validate(token)
    val data  =? transform(input)
    val user  =? db.persist(data)
    Ok(user)

=? propagiert sofort – db.persist wird nie erreicht wenn ein vorheriger Schritt fehlschlägt. Transactional ist nur nötig wenn mehrere persistierende Operationen zusammen atomar sein müssen.


5. Deklarative Runtime – Alles ist ein Dictionary

5.1 Die fundamentale Einsicht

Jeder deklarative Konstrukt in JDL – CallGraph, Aktor, Workflow, UI-Komponente – ist zur Laufzeit ein composable Dictionary. Die Effect-Engine arbeitet immer mit demselben Mechanismus.

Das ist die direkte Laufzeit-Entsprechung der Compile-Zeit Meta-Records:

Compile-Zeit:  <{ key: value }>   – Meta-Record, statisch
Laufzeit:      { key: value }     – Dictionary, dynamisch

Dasselbe Konzept, zwei Ebenen, strukturelle Konsistenz.

5.2 Konsequenzen des Dictionary-Designs

Serialisierbarkeit ist eine strukturelle Eigenschaft, keine Feature-Anforderung. Ein Dictionary ist trivial serialisierbar. Jeder deklarative Konstrukt kann persistiert, übertragen, deserialisiert und auf einem anderen System ausgeführt werden.

Komposabilität folgt direkt. Neue deklarative Konstrukte brauchen kein neues VM-Konzept – nur bekannte Keys die die Effect-Engine interpretiert.

Reflexion wird trivial. Ein laufender CallGraph kann sich selbst inspizieren weil er nur ein Dictionary ist – dynamische Rekonfiguration, Monitoring und Introspection ohne spezielle VM-Unterstützung.

Reproduzierbarkeit ist garantiert. Denselben serialisierten Graphen zweimal ausführen produziert dasselbe Ergebnis.

5.3 Distributed Execution als natürliche Konsequenz

Hot Deployment – einen neuen CallGraph serialisiert über das Netzwerk schicken und auf einem laufenden Node einsetzen ohne Restart.

Distributed Workflows – ein Workflow-Graph über mehrere Nodes verteilt. Die Knoten kommunizieren über das Dictionary-Protokoll, sie wissen nicht voneinander.

Remote Actors – kommunizieren transparent, weil beide nur serialisierbare Dictionaries sind. Der Unterschied zwischen lokalem und remoten Aktor ist ein Implementierungsdetail der Stdlib.

Diese Möglichkeiten sind für Phase 2 vorgesehen. Phase 1 implementiert den Interpreter. Das Design lässt die Tür offen – ohne Nacharbeit.

5.4 Verhältnis zu Meta-Records

// Compile-Zeit – Meta-Record beschreibt Verhalten:
type <{ cache: CacheConfig { ttl: 24.hours }, share: Sync }> UserCache: struct { ... }

// Laufzeit – Dictionary trägt dieselbe Information:
{ cache: { ttl: 86400 }, share: "Sync", ... }

6. Normative Invarianten

  1. provide ... from existiert nicht. Konversion läuft ausschließlich über CastTo[T].
  2. .castTo() ohne expliziten Typparameter ist bei Mehrdeutigkeit ein Compile-Fehler.
  3. Cast-Modi liegen auf der Implementierung, nicht auf dem Aufruf.
  4. Konversionen sind nie transitiv. Kein automatisches A -> C wenn A -> B und B -> C existieren.
  5. Effect[R, E, D] ist ein normaler generischer Typ in der Stdlib — kein Compiler-Primitiv. Der Compiler hat kein Sonderwissen darüber. R = Erfolgstyp, E = Fehlertyp, D = Dependencies. Result ist im R-Kanal nicht einzuschachteln.
  6. Effects werden ausschließlich in der Effect-Engine (CallGraph) interpretiert. Außerhalb ist alles Beschreibung.
  7. deps als Sprachkonstrukt auf Funktionsebene existiert nicht. Imperative Funktionen erhalten Abhängigkeiten als Parameter. Effect-Funktionen deklarieren sie im Typ D.
  8. Ein Effect-Wert entsteht durch normale Typkomposition in der Stdlib — kein Compiler-Sonderfall. Die gesamte Kompositions-API (map, flatMap, provide, retry, ...) ist JDL-Code der auf Effect[R, E, D] als gewöhnlichem Typ aufbaut.
  9. Cache-Key = Funktionssignatur + Hash(Parameter). Parameter müssen Hashable implementieren.
  10. Die Ausführungsreihenfolge von Funktions-Refinements legt der Compiler fest, nicht der Entwickler.
  11. Transactional impliziert atomares Rollback aller Operationen im Block.
  12. Effect-Interpretation ist eine Laufzeit-Verantwortung der Stdlib, nicht des Compilers. Optimierungen (AOT, Inlining) sind allgemeine Compiler-Optimierungen — kein Effect-spezifisches Feature.
  13. Jeder deklarative Konstrukt ist zur Laufzeit ein composable Dictionary.
  14. Serialisierbarkeit ist eine strukturelle Eigenschaft aller deklarativen Konstrukte.
  15. D ist kein Bestandteil der JDL-Architektur. Es ist der einmalige Bootstrap-Mechanismus.
  16. Die Stdlib ist vollständig in JDL geschrieben und baut auf @intrinsic-Operationen in der rohen Runtime-Brücke jade::vm sowie auf user-facing Frontends in jdl::... auf.

7. Offene Punkte für Phase 2

  • Effect Polymorphism – mehrere Interpreter für denselben Effect-Typ
  • Deklarative API für Aktoren, Workflows und UI auf Basis des Effect-Systems
  • CircuitBreaker als weiteres Funktions-Refinement
  • Stdlib-Implementierung von Effect[R, E, D] — Monad-Gesetze und Lawfulness als Stdlib-Verantwortung
  • Dictionary-Schema-Validierung zur Laufzeit
  • Netzwerk-Transport-Protokoll für serialisierte Graphen
  • Hot Deployment Semantik und Sicherheitsmodell
  • Formale Definition der Grenzen zwischen jdl- und jade-Namespace