JDL Grundlagen — Vom Script zur Anwendung¶
Fühlt sich an wie Python. Fängt Fehler wie Rust. Skaliert wie beides nicht.
JDL ist keine kompilierte Sprache im engeren Sinne — sie kann als Scripting-Sprache verwendet werden, Bytecode exportieren, embedden und deployen. Dieses Dokument zeigt den Weg von einem einfachen Script zu einer strukturierten Anwendung.
Durchgehendes Beispiel: Wir bauen Schritt für Schritt ein User-Management-System — von der ersten Zeile bis zur vollständigen Domänenlogik. Jedes Sprachfeature kommt dazu wenn es gebraucht wird.
Änderungstabelle¶
| Alt (Spezifikation) | Neu (dieses Dokument) |
|---|---|
fn |
def |
fn(T) -> U (Typ) |
(T) -> U / T -> U (Einzel-Arg) |
&& \|\| ! |
and or not (Aliases — && \|\| ! bleiben gültig) |
"a" ++ x.toString() |
f"a{x}" / f"""...""" (multiline) |
let x = |
val x = (immutabel) / var x = (mutabel) |
type X: enum { A, B } |
type X: enum = \| A \| B (neue Atome) |
arm => expr, |
\| arm => expr |
{ x => expr } (Closure) |
x => expr / { x => ... } (Block) |
derive(Equatable, Hashable) |
<{ derive: [Equatable, Hashable] }> (Präfix) / :> Derive([Equatable, Hashable]) (Suffix) |
f(name: "Alice") (Aufruf) |
f(name = "Alice") |
| (kein Inline-if) | if cond then x else y |
Unverändert: :> / <{ }> Meta-Records (siehe Dokument 0: Refinement-System),
=?, where, typefn, service, CallGraph, Generics
1. Hello World — Werte und Ausgabe¶
val bindet immutable. f"..." interpoliert Ausdrücke in {...}.
Das ist ein vollständiges JDL-Script — direkt ausführbar, kein Boilerplate.
val vs. var¶
val name = "Alice" // immutabel — kann nicht überschrieben werden
var counter = 0 // mutabel — kann überschrieben werden
counter = counter + 1 // ok
name = "Bob" // COMPILERFEHLER: val ist immutabel
Kein const nötig¶
Ein val mit Literal-Zuweisung ist für den Compiler trivial als
Compile-Zeit-Konstante erkennbar. Für explizite Garantien existiert
das Typsystem:
type MaxRetries = 3 // Literal-Typ — genau ein Inhabitant
type HttpSuccessCode = 200 | 201 | 204 // Union von Literalen
val maxRetries = 3 // Compiler optimiert als Konstante
val defaultTimeout = 5000ms // Unit-Literal
2. Funktionen¶
// Einzeilig:
def double(x: i32) -> i32 = x * 2
// Block:
def greet(name: str) -> str {
val msg = f"Hello, {name}!"
msg
}
// Letzter Ausdruck ist Rückgabewert — kein `return` nötig
Generics¶
def identity[T](x: T) -> T = x
def mapList[T, U](xs: [T], f: T -> U) -> [U] {
var out: [U] = []
for item in xs {
out.push(f(item))
}
out
}
4. Unser Domänen-Modell — Structs¶
Jetzt starten wir mit dem User-Management. Zuerst die Daten:
type User: struct {
name: str
email: str
age: i32
}
val alice = User { name: "Alice", email: "alice@example.com", age: 30 }
Console.print(f"{alice.name} ist {alice.age} Jahre alt")
Keine Kommas zwischen Feldern — Zeilenende ist Trennzeichen.
Die :>-Annotation setzt Policies — dazu mehr in Teil 2:
type <{
derive: [Equatable, Hashable]
memory: Value
share: Sync
drop: Trivial
}> Point: struct {
x: f64
y: f64
}
5. Enums — Varianten und Unions¶
Einfache Varianten¶
Varianten mit Payload¶
Varianten mit benannten Feldern¶
Für unser System brauchen wir Fehlerwerte:
type UserError: enum =
| NotFound { id: str }
| InvalidEmail { email: str, reason: str }
| AlreadyExists { email: str }
Union existierender Typen¶
Unions werden automatisch flach — Duplikate entfernt:
6. Fehlerbehandlung — Result und =?¶
def validateEmail(email: str) -> Result[str, UserError] =
if email.contains("@") then
Ok(email)
else
Err(UserError.InvalidEmail { email, reason: "Kein @-Zeichen" })
def validateAge(age: i32) -> Result[i32, UserError] =
if age >= 0 and age <= 150 then
Ok(age)
else
Err(UserError.InvalidAge { age, reason: "Ausserhalb des gültigen Bereichs" })
=? propagiert Fehler sofort — ohne verschachtelte match:
def createUser(name: str, email: str, age: i32) -> Result[User, UserError] =
val validEmail =? validateEmail(email)
val validAge =? validateAge(age)
Ok(User { name, email: validEmail, age: validAge })
7. Pattern Matching¶
def describeUser(user: User) -> str =
match user.role {
| Role.Admin => f"{user.name} ist Administrator"
| Role.Moderator => f"{user.name} ist Moderator"
| Role.User => f"{user.name} ist normaler Nutzer"
}
def handleResult(res: Result[User, UserError]) -> str =
match res {
| Ok(user) => f"OK: {user.name}"
| Err(UserError.NotFound { id }) => f"Nicht gefunden: {id}"
| Err(UserError.AlreadyExists { email }) => f"Existiert bereits: {email}"
| Err(e) => f"Fehler: {e}"
}
Match ist exhaustiv — der Compiler meldet fehlende Arme.
8. Option und Optional Chaining¶
def findUser(users: [User], email: str) -> Option[User] =
users |> filter(u => u.email == email) |> first
val user: Option[User] = findUser(users, "alice@example.com")
// Optional Chaining:
val street = user?.address?.street // -> Option[str]
val city = user?.address?.city ?? "N/A" // mit Fallback via ??
?? ist der Nil-Coalescing-Operator: opt ?? default entpackt oder nimmt Default.
9. Pipelines |>¶
val adminNames =
users
|> filter(u => u.active and u.role == Role.Admin)
|> map(_.name)
|> sort
val firstAdmin = adminNames |> first
10. Lambdas¶
// Concise:
users |> filter(u => u.active)
users |> map(u => u.name.trim())
// Placeholder:
users |> filter(_.active)
users |> map(_.name)
// Block:
users |> map { u =>
val name = u.name.trim()
f"{name} ({u.role})"
}
11. Strings¶
// Interpoliert:
val msg = f"Hallo, {alice.name}! Du bist {alice.age} Jahre alt."
// Raw — keine Escapes (Regex, Pfade):
val pattern = r"\d+\.\d+"
val path = r"C:\Users\alice\Documents"
// Multi-line:
val sql = """
SELECT name, email
FROM users
WHERE age >= 18
"""
// Multi-line interpoliert:
val query = f"""
SELECT name, email
FROM users
WHERE age >= {minAge}
LIMIT {limit}
"""
Einrückung wird normalisiert.
12. Collections¶
Arrays und Slices¶
val names: [str] = ["Alice", "Bob", "Charlie"]
val first = names[0]
val len = names.len()
// Statisch großes Array:
val rgb: [u8; 3] = [255u8, 128u8, 0u8]
Tuples¶
val pos: (f64, f64) = (52.52, 13.405)
val (lat, lon) = pos
def minMax(xs: [i32]) -> (i32, i32) = (xs.min(), xs.max())
val lo, hi = minMax([3, 1, 4, 1, 5])
13. Services und Effekte¶
Unser User-System braucht persistente Speicherung. Statt einer konkreten Datenbank definieren wir ein abstraktes Interface:
service UserDb {
def getUserById(id: str) -> Result[User, UserError]
def getUserByEmail(email: str) -> Result[User, UserError]
def insertUser(user: User) -> Result[User, UserError]
def listUsers() -> Result[[User], UserError]
}
service LogService {
def debug(msg: str) -> ()
def info(msg: str) -> ()
def warn(msg: str) -> ()
def error(msg: str) -> ()
}
Zwei Stile — eine Sprache¶
JDL kennt kein deps-Konstrukt auf Funktionsebene. Abhängigkeiten werden
entweder als Parameter übergeben oder über Effect[R, E, D] im Rückgabetyp
deklariert. Beide Stile sind vollwertig und gleichberechtigt.
Effect[R, E, D] ist ein normaler generischer Typ in der Stdlib — kein
Compiler-Primitiv. Der Compiler hat kein Sonderwissen darüber. Die gesamte
Semantik — Komposition, Retry, Remote-Ausführung, Concurrency — ist JDL-Code
der auf diesem Typ aufbaut, genau wie Effect-TS TypeScript nur benutzt statt
es zu erweitern.
Imperativer Stil — Services als Parameter, sofortige Ausführung:
def findUser(db: UserDb, log: LogService, email: str) -> Result[User, UserError] =
log.debug(f"Suche User: {email}")
db.getUserByEmail(email)
def registerUser(
db: UserDb
log: LogService
name: str
email: str
age: i32
) -> Result[User, UserError] =
val validEmail =? validateEmail(email)
val validAge =? validateAge(age)
val user = User { name, email: validEmail, age: validAge }
val saved =? db.insertUser(user)
log.info(f"Registriert: {saved.name} <{saved.email}>")
Ok(saved)
Effect-Stil — lazy Beschreibung, Dependencies im Typ, Runtime-Komposition:
def findUser(email: str) -> Effect[User, UserError, [UserDb, LogService]] =
LogService.debug(f"Suche User: {email}")
UserDb.getUserByEmail(email)
def registerUser(
name: str
email: str
age: i32
) -> Effect[User, UserError, [UserDb, LogService]] =
val validEmail =? validateEmail(email)
val validAge =? validateAge(age)
val user = User { name, email: validEmail, age: validAge }
val saved =? UserDb.insertUser(user)
LogService.info(f"Registriert: {saved.name} <{saved.email}>")
Effect.ok(saved)
Der Funktionskörper sieht in beiden Stilen nahezu identisch aus. Der Unterschied:
der imperative Stil gibt einen Wert direkt zurück, der Effect-Stil gibt einen
Effect-Wert zurück der erst durch explizites Bereitstellen der Dependencies
ausgeführt wird. Das ist normale Typsemantik — kein Compiler-Sonderfall.
CallGraph — Wiring und Runtime-Verhalten¶
CallGraph verdrahtet Dependencies und ist eine echte serialisierbare
Datenstruktur — kein Compiler-Mechanismus. Runtime-Verhalten wie Retry,
Remote-Ausführung und Concurrency sind Refinements auf dem CallGraph:
CallGraph handleRequest(req: HttpRequest) -> Result[HttpResponse, AppError] {
requires: [UserDb, LogService]
env: {
UserDb: InMemoryUserDb { users: [] }
LogService: ConsoleLogger {}
}
Node handle { call: routeRequest(req) }
flow: handle
} :> Retry(max: 3, backoff: Exponential)
:> Serializable
Weil der CallGraph eine Datenstruktur ist, kann er inspiziert, transformiert, geloggt, serialisiert und remote ausgeführt werden. Das ist kein Compiler-Feature — es ist die Stdlib die auf einem normalen Typ aufbaut.
Handler — konkrete Implementierung¶
type InMemoryUserDb: struct {
users: [User]
}
provide UserDb for InMemoryUserDb {
def getUserByEmail(self, email: str) -> Result[User, UserError] =
match self.users |> filter(u => u.email == email) |> first {
| Some(user) => Ok(user)
| None => Err(UserError.NotFound { id: email })
}
def insertUser(self, user: User) -> Result[User, UserError] =
match self.getUserByEmail(user.email) {
| Ok(_) => Err(UserError.AlreadyExists { email: user.email })
| Err(_) => {
self.users.push(user)
Ok(user)
}
}
// ... weitere Methoden
}
CallGraph — Service-Wiring¶
CallGraph ist eine Datenstruktur die Dependencies verdrahtet und
Runtime-Verhalten deklariert. Sie ist kein Compiler-Mechanismus — der Compiler
prüft nur ob alle requires-Einträge in env vorhanden sind.
CallGraph handleRequest(req: HttpRequest) -> Result[HttpResponse, AppError] {
requires: [UserDb, LogService]
env: {
UserDb: InMemoryUserDb { users: [] }
LogService: ConsoleLogger {}
}
Node handle { call: routeRequest(req) }
flow: handle
}
Weil CallGraph eine echte Datenstruktur ist, können Runtime-Behaviors als
Refinements hinzugefügt werden — ohne den Compiler zu berühren:
CallGraph handleRequest(...) { ... }
:> Retry(max: 3, backoff: Exponential)
:> Serializable
:> Remote(endpoint: "worker-pool")
Das gesamte Verhalten — Retry, Remote-Ausführung, Concurrency — ist
Stdlib-Code der auf CallGraph als normalem Typ aufbaut.
14. Function Unions¶
Unser Service-Interface lässt sich auch als Function Union ausdrücken — nützlich für Actor-Messages und RPC:
type UserCommand =
| def register(name: str, email: str, age: i32) -> Result[User, UserError]
| def find(email: str) -> Result[User, UserError]
| def list() -> Result[[User], UserError]
| def delete(email: str) -> Result[(), UserError]
Ein Wert vom Typ UserCommand repräsentiert genau eine dieser
Operationen inklusive ihrer Argumente.
15. Dokumentationskommentare¶
/// Registriert einen neuen Benutzer im System.
///
/// Validiert E-Mail und Alter bevor der User in die Datenbank
/// geschrieben wird. Schlägt fehl mit `AlreadyExists` wenn die
/// E-Mail bereits vergeben ist.
def registerUser(db: UserDb, log: LogService, name: str, email: str, age: i32) -> Result[User, UserError]
/// über einer Deklaration — von Tooling (LSP, Docs-Generator)
auswertbar.
Zusammenfassung Teil 1¶
Wir haben ein User-Management-System mit folgenden Features:
Werte: val (immutabel), var (mutabel)
Funktionen: def f(x: T) -> U = expr / def f(x: T) -> U { ... }
Typen: struct, enum (type X: enum = | A | B), union (type X: union = A | B)
Fehler: Result[T, E], =? für Propagation
Optional: Option[T], ?. für Chaining, ?? für Fallback
Pattern Match: match expr { | Arm => ... } — exhaustiv
Pipelines: |> für Datentransformationen
Lambdas: x => expr, _.field, { x => ... }
Strings: f"...", r"...", """..."""
Services: service, provide
Imperativer Stil: Services als Parameter — sofortige Ausführung
Effect-Stil: Effect[R, E, D] — normaler Stdlib-Typ, lazy
CallGraph: Wiring-Datenstruktur — Refinements für Retry, Remote, Concurrency
Im nächsten Teil erweitern wir unser System mit dem Typsystem: Protocols, Operator-Überladung, Typ-Konversion und Phantom Types.