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:
- Konversionen werden nicht länger als Sonderform von
providebehandelt, sondern als gewöhnliches Protocol mit Operator-Bindung –CastTo[T]via->. - 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. Dasdeps-Konstrukt auf Funktionsebene entfällt vollständig. - 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 provide – provide 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:
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:
Effect-Stil — Effect[R, E, D] als normaler Rückgabetyp, lazy durch den Typ selbst:
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¶
provide ... fromexistiert nicht. Konversion läuft ausschließlich überCastTo[T]..castTo()ohne expliziten Typparameter ist bei Mehrdeutigkeit ein Compile-Fehler.- Cast-Modi liegen auf der Implementierung, nicht auf dem Aufruf.
- Konversionen sind nie transitiv. Kein automatisches
A -> CwennA -> BundB -> Cexistieren. 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.Resultist im R-Kanal nicht einzuschachteln.- Effects werden ausschließlich in der Effect-Engine (CallGraph) interpretiert. Außerhalb ist alles Beschreibung.
depsals Sprachkonstrukt auf Funktionsebene existiert nicht. Imperative Funktionen erhalten Abhängigkeiten als Parameter. Effect-Funktionen deklarieren sie im TypD.- 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 aufEffect[R, E, D]als gewöhnlichem Typ aufbaut. - Cache-Key = Funktionssignatur + Hash(Parameter). Parameter müssen
Hashableimplementieren. - Die Ausführungsreihenfolge von Funktions-Refinements legt der Compiler fest, nicht der Entwickler.
Transactionalimpliziert atomares Rollback aller Operationen im Block.- Effect-Interpretation ist eine Laufzeit-Verantwortung der Stdlib, nicht des Compilers. Optimierungen (AOT, Inlining) sind allgemeine Compiler-Optimierungen — kein Effect-spezifisches Feature.
- Jeder deklarative Konstrukt ist zur Laufzeit ein composable Dictionary.
- Serialisierbarkeit ist eine strukturelle Eigenschaft aller deklarativen Konstrukte.
- D ist kein Bestandteil der JDL-Architektur. Es ist der einmalige Bootstrap-Mechanismus.
- Die Stdlib ist vollständig in JDL geschrieben und baut auf
@intrinsic-Operationen in der rohen Runtime-Brückejade::vmsowie auf user-facing Frontends injdl::...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
CircuitBreakerals 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- undjade-Namespace