JDL Notiz: Typalgebra, Records, Blueprints und semantische Typmaterialisierung¶
Status: Arbeitsnotiz / nach Review überarbeitet
Datum: 2026-05-26
Ziel: Konsolidierung der Diskussion zu Alias, Newtype, Records, &, Blueprints, Funktionstypen, TypeKinds, Row-Polymorphismus, Effect Rows, Associated Types, Zero-field Newtypes und Variance.
Diese Notiz ist keine normative Spezifikation. Sie beschreibt den aktuellen Arbeitskonsens und soll später gegen die bestehenden JDL/Jade-Spezifikationen konsolidiert werden.
1. Grundthese¶
JDL sollte Typen nicht nur als Namen, sondern als algebraisch normalisierbare Beschreibungen behandeln.
Die zentrale Schichtung:
Record = strukturelle Feldform, keine eigene nominale Identität
Struct = nominaler Datentyp über einer Record-Form
Blueprint = nominaler Beschreibungstyp über einer Record-Form
MetaRecord = Compile-Time-Record für Policies, Refinements und TypeFacts
Alias = transparenter Name für einen Typausdruck
Newtype = neue nominale TypeId über einem Carrier
Tag = reine Compile-Time-Bedeutung ohne Wert
Kurz:
Record definiert Form.
Struct/Blueprint definieren Interpretation.
Alias koppelt an einen bestehenden Typ.
Newtype entkoppelt durch neue nominale Identität.
Tag benennt Bedeutung ohne Runtime-Wert.
Der zentrale Entwurfsgrundsatz:
Daraus folgt eine wichtige operative Regel für & und Re-Materialisierung:
Struct und Blueprint sind RecordShapes mit Identität und Interpretation.
Strukturelle Operationen wie & operieren auf der Record-Form-Schicht.
Identität und Interpretation kommen erst durch Materialisierung wieder dazu.
2. Alias und Newtype¶
Alias¶
Ein Alias ist transparent:
Eigenschaften:
- keine neue TypeId
- keine neue Coherence-Heimat
- keine neue Memory-Semantik
- keine neue Protocol-Identität
- nur ein weiterer Name für denselben Typausdruck
Aliases eignen sich als Carrier-Schablonen:
Newtype¶
JDL verwendet das eigene Keyword newtype für die Materialisierung einer neuen nominalen Identität über einer Form:
Ein Newtype erzeugt eine neue nominale Identität:
Eigenschaften:
- neue TypeId
- eigene Coherence-Heimat
- eigener Protocol-/Methoden-Raum
- gleiche zugrunde liegende Repräsentation wie der Carrier
- Ring-0/Core-Facts werden übernommen oder neu aus dem Carrier abgeleitet
- API, Provides und domänenspezifische Semantik werden nicht automatisch übernommen
Merksatz:
newtype ist die einzige Form, einen Newtype zu erstellen. Die ältere ML-artige Schreibweise type X = X(Y) wird in JDL nicht mehr verwendet — sie ist didaktisch missverständlich (Namens-Wiederverwendung sieht wie Rekursion aus) und bietet keinen Mehrwert, weil JDL ADTs mit mehreren Konstruktoren über enum ausdrückt.
Spezialisierung bei der Newtype-Materialisierung¶
Bei der Materialisierung können Felder über & spezialisiert werden:
newtype EnrichedUser = User & { roles: [Role] }
newtype DefaultedUser = User & { name: str = "anonymous" }
Die rechte Seite des & kann strukturell erweitern (neue Felder hinzufügen) oder Default-Werte für existierende Felder vorbelegen. Default-Override ist die Standardbedeutung; feste Wertbindung (Konstantisierung von Feldern) wird nicht im Sprachkern abgebildet und ist Stdlib-Sache über Refinements.
Alias als Schablone, Newtype als Materialisierung¶
type IdCarrier = str :> NonEmpty :> MaxLen(64)
newtype UserId = IdCarrier
newtype OrderId = IdCarrier
newtype PostId = IdCarrier
IdCarrier definiert die wiederverwendbare Form. Die Newtypes materialisieren daraus eigenständige Domänen.
3. Record, Struct und Blueprint¶
Ein Record ist eine strukturelle Produktform:
Ein Struct materialisiert eine Record-Form als Datentyp:
Ein Blueprint materialisiert eine Record-Form als Beschreibungstyp:
type RouteSpec: blueprint {
method: HttpMethod = .GET
path: str
handler: (HttpRequest) -> HttpResponse
}
Der Unterschied liegt nicht in der Feldform, sondern im TypeKind und in der Interpretation:
Struct-Felder = Laufzeitdaten
Blueprint-Felder = deklarative Beschreibung / Engine-Input
MetaRecord = Compile-Time-Policy
Ein Record selbst ist noch kein nominaler Typ. Er ist ein Shape. Erst Struct, Blueprint oder Newtype materialisieren daraus einen konkreten Typ mit TypeDescriptor/TypeId.
Wichtig für strukturelle Operationen: Struct und Blueprint enthalten ihre Record-Form. Operationen wie & operieren auf dieser Form-Schicht, nicht auf der TypeKind-Wrapper-Schicht. Das Ergebnis von Struct & {...} oder Blueprint & {...} ist wieder ein RecordShape, der durch erneute Materialisierung (Newtype-Konstruktor, Struct-/Blueprint-Definition) eine neue Identität bekommen kann. So bleibt & eine Operation auf einer einzigen Schicht, statt eine pro TypeKind-Kombination.
4. Enum-Member-Kurzschreibweise¶
JDL kann eine führende-Punkt-Schreibweise für Enum-Member verwenden:
type AdminRouteSpec: blueprint {
method: HttpMethod = .GET
auth: AuthPolicy = .Required
audit: AuditPolicy = .Enabled
cache: CachePolicy = .Disabled
path: str
handler: (HttpRequest) -> HttpResponse
}
Regel:
Desugaring:
.GET => HttpMethod.GET
.Required => AuthPolicy.Required
.Enabled => AuditPolicy.Enabled
.Disabled => CachePolicy.Disabled
Ohne erwarteten Enum-Typ ist .Member ein Compilerfehler.
5. Der &-Operator¶
& ist ein type-level Operator für strukturelle Komposition und Spezialisierung. Er operiert auf der Record-Form-Schicht: Struct, Blueprint und RecordShape reichen ihre Form an & durch, das Ergebnis ist wieder ein RecordShape.
Grundregel¶
bedeutet bei strukturellen Formen:
Also: rechts überschreibt links, wenn die Kollision kompatibel ist.
Record-Merge¶
normalisiert zu:
Kollisionsregeln:
Feld nur links: übernehmen
Feld nur rechts: übernehmen
gleiches Feld, gleicher Typ: rechter Default/Wert gewinnt
gleiches Feld, inkompatibler Typ: Compilerfehler
Beispiel:
normalisiert zu:
Leerer Record als neutrales Element¶
normalisiert zu:
Algebraisch:
Der leere Record {} ist das neutrale Element für Record-&.
Alias vs. Newtype mit &¶
type Shape = { a: i32 } & { b: str }
Alias, keine neue TypeId
newtype NamedShape = { a: i32 } & { b: str }
Newtype, neue TypeId
Blueprint-Spezialisierung¶
type EffectBase[R, E, D]: blueprint {
inputs: D
run: (D) -> Result[R, E]
env: D
}
newtype UserRegistrationEffect =
EffectBase[User, UserError, UserDeps] & {
run: deps => registerUser(deps)
}
:> Retry(max: 3)
:> Timeout(30s)
Bedeutung:
EffectBase[...] definiert die Blueprint-Form.
& { run: ... } konkretisiert/spezialisiert diese Form.
newtype materialisiert daraus eine neue nominale Blueprint-Domäne.
:> setzt Policies auf diese Domäne.
Bei Blueprint-Spezialisierung über & sind zwei Fälle zu unterscheiden, die nicht vermischt werden dürfen:
Fall A — Konservative Spezialisierung des Blueprint-Typs.
Wenn die Materialisierung als Spezialisierung desselben Blueprint-Typs gemeint ist, darf die rechte Seite nur existierende Felder vorbelegen oder kompatibel überschreiben. Der resultierende Newtype bleibt strukturell ein EffectBase[...].
Fall B — Erweiterter Newtype über erweitertem Shape.
Wenn die rechte Seite neue Felder einführt, entsteht ein Newtype über einem erweiterten RecordShape. Der Newtype ist dann strukturell nicht mehr identisch mit EffectBase[...], sondern hat eigene Felder zusätzlich.
Beide Fälle sind legitim, aber sie haben unterschiedliche Konsequenzen für Type-Compatibility und Engine-Dispatch. Welcher Fall gemeint ist, ergibt sich aus den rechten &-Operanden — neue Felder verschieben automatisch von A nach B. Die Spec sollte das normativ benennen, damit Leser nicht in eine "Ausnahmeregel"-Lesart verfallen, obwohl es nur die Konsequenz der Record-Algebra ist.
6. Zero-field Newtypes, Tags und Witness Tokens¶
Ein Newtype über dem leeren Record {} erzeugt einen materialisierbaren Werttyp ohne Nutzdaten:
Eigenschaften:
- eigene TypeId
- runtime-materialisierbarer Wert
- keine relevante Payload
- typischerweise Unit-/Zero-size-Repräsentation
- kann als Wert übergeben, verlangt oder gespeichert werden
Das ist nicht dasselbe wie ein Tag.
Ein Tag ist eine reine Compile-Time-Bedeutung:
Abgrenzung:
tag AdminOnly
Bedeutung ohne Wert.
Compile-time only.
newtype AdminToken = {}
Wert ohne Nutzdaten.
Runtime-materialisierbar.
Einsatzgebiete¶
Zero-field Newtypes eignen sich für:
- Witness Values
- Proof Tokens
- Capability Tokens
- State Tokens
- API-Gates
- kontrollierte Berechtigungsübergabe
Beispiel:
Die Funktion verlangt einen konkreten Beweis-/Berechtigungswert. Dieser Wert trägt keine Nutzdaten, aber sein Typ ist relevant.
Regel:
Wenn keine Runtime-Existenz gebraucht wird: tag.
Wenn ein konkreter Beweis-/Berechtigungswert übergeben werden soll: zero-field Newtype.
Wenn ein Werttyp eine Bedeutung nur auf Type-Level tragen soll: phantom tag parameter.
Beispiel für Phantom-Tags:
Hier existieren Registered und Verified nicht als Werte. Sie klassifizieren den Typ auf Compile-Time-Ebene.
7. Funktionen als Typen¶
Funktionstypen sind normale Typausdrücke:
Blueprint-Felder dürfen echte Signaturen haben:
Ein Funktionsfeld kann implementiert werden durch:
- FQN-Funktionsreferenz
- inline Blueprint-lokalen Funktionskörper
Beispiel mit FQN:
newtype UserRegistrationEffect =
EffectBase[User, UserError, UserDeps] & {
run: myapp::users::registerUser
}
Beispiel inline:
newtype UserRegistrationEffect =
EffectBase[User, UserError, UserDeps] & {
run: deps => {
deps.db.register(...)
}
}
Regel für Inline-Funktionen in Blueprints:
Sie dürfen keine äußeren lexikalischen Bindings capturen.
Sie dürfen nur referenzieren:
- eigene Parameter
- Blueprint-Felder
- FQN/importierte Symbole
- im Context verfügbare Services/Capabilities
Blueprint-Felder und Auflösung¶
Blueprint-Felder sind nach Initialisierung read-only. Sie tragen Compile-Time-Werte (Literale, FQN-Symbole, andere bereits aufgelöste Blueprint-Felder, Funktionswerte). Sie sind statische Konfiguration, kein veränderlicher Runtime-State.
Referenzen auf Blueprint-Felder aus Inline-Funktionen werden bei der Materialisierung des Newtypes aufgelöst und im resultierenden Funktionswert eingebacken. Die Auflösungs-Reihenfolge ist topologisch nach Abhängigkeiten zwischen den Feldern; zyklische Referenzen sind ein Compile-Fehler.
Echter mutierender State gehört nicht ins Blueprint, sondern in die Dependency-Row. Eine Funktion, die einen Cache oder eine DB-Verbindung braucht, deklariert das nicht als Blueprint-Feld, sondern als Service in D und greift über den deps-Parameter darauf zu. Damit bleiben Blueprints deklarative Beschreibungen statt Instanzen mit eingebautem State.
Ein capture-freier Funktionswert ist als CallDescriptor serialisierbar, wenn Parameter, Rückgabetyp, Constraints und referenzierte Symbole im Ziel-Context auflösbar sind. Da Blueprint-Feld-Referenzen bei Materialisierung substituiert werden, bleibt die Inline-Funktion auch nach Materialisierung capture-frei im lexikalischen Sinn.
Kernsatz:
Blueprint-Feld:
echte Typsignatur, read-only, Compile-Time-Wert
Engine:
verlangt Implementierung dieser Signatur
Implementierung:
inline oder per FQN
State:
explizit in der Dependency-Row, nicht im Blueprint
Auflösung:
bei finaler Newtype-Materialisierung, topologisch, eingebacken
8. Type-Level-Normalisierung¶
Typausdrücke werden vor TypeId-Vergabe, Constraint-Prüfung und Descriptor-Erzeugung normalisiert.
Beispiele:
A | B | A => A | B
{ a } & { b } => { a, b }
type RawId = str; newtype UserId = RawId => UserId(str)
Aber:
bleibt UserId(RawId), weil RawId ein Newtype ist.
Invarianten:
Alias wird aufgelöst.
Newtype bleibt nominale Grenze.
Union wird flach, dedupliziert und stabil geordnet.
Record-& wird kanonisch gemerged.
Refinements werden in kanonische MetaRecord-Form gebracht.
Normalisierung ist Grundlage für:
- TypeId-Stabilität
- CompilerDb-Cache-Keys
- Coherence-Prüfung
- Descriptor-Erzeugung
- bessere Diagnostics
Ohne Normalisierung werden logisch gleiche Typen intern verschieden. Das ist keine Designfreiheit, das ist später ein Bug mit akademischem Hut.
9. TypeKind / Kinds / Bootstrap Seed¶
JDL braucht kein neues oberflächliches Kind-System. Die vorhandenen Ring-0/TypeEngine-TypeKinds bilden die semantischen Kinds des Typsystems.
TypeKind sagt: Was ist das?
Labels sagen: Was gilt dafür?
Protocols sagen: Was kann es?
Refinements sagen: Welche Policy wurde gesetzt?
Beispiele für TypeKinds:
Primitive
Struct
Enum
Union
Function
Tuple
Blueprint
Flags
RecordShape
MetaRecord
Protocol
Service
Newtype
Alias
TypeFns und Typalgebra prüfen gegen TypeKind + Ring-0-Facts.
Beispiele:
Pick/Omit verlangen RecordShape-kompatible Typen.
Blueprint-Spezialisierung verlangt Blueprint-kompatible TypeKinds.
Newtype verlangt einen materialisierbaren Carrier.
& verlangt kompatible strukturelle TypeKinds (operiert auf der Record-Form-Schicht).
Bootstrap Seed¶
JDL-Primitives entstehen nicht durch normale User-Typdefinitionen. Sie werden im Bootstrap Seed als kanonische TypeDescriptors registriert.
Ring 0 liefert unverhandelbare Facts:
- Repräsentation
- Größe
- Alignment
- Registerklasse
- Immediate/Handle-Status
- primitive Operationsklasse
Der Bootstrap Seed materialisiert daraus JDL-sichtbare TypeIds und Namen.
Aliases können zusätzliche Namen auf denselben Descriptor legen. Newtypes erzeugen neue TypeIds über einem Carrier.
Schichtung:
Ring 0 / Jade Core:
Primitive Facts, TypeId, TypeKind, TypeDescriptor, JME, VM, Intrinsics
Bootstrap Seed:
minimale JDL-Typen, Primitive-Namen, Result, Option, Loader-Diagnostics
Stdlib / JDL:
Protocols, Effects, Rows, Associated Types, Profiles, Collections, Engines
Userland / Plugins:
konkrete Blueprints, Services, Domain-Typen, externe Integrationen
10. Row-Polymorphismus¶
Row-Polymorphismus erlaubt strukturelle Constraints auf Typen, ohne den exakten nominalen Typ zu verlangen.
Bedeutung:
Das trennt Struktur von Verhalten:
Nested Constraints sind möglich:
oder mit separatem Feldtyp-Constraint:
Der eigentliche Nutzen ist, dass Constraints auf unterschiedlichen Ebenen des Typs gezogen werden können:
TypeKind-Ebene:
T ist Struct / Blueprint / RecordShape / Function / ...
Protocol-/Label-Ebene:
T erfüllt Serializable + Send
Row-/Shape-Ebene:
T hat bestimmte Felder
Feldtyp-Ebene:
T.x hat Typ X oder erfüllt Constraint C
Nested-Shape-Ebene:
T.a.b.c existiert mit bestimmter Struktur
Meta-/Refinement-Ebene:
T oder ein Feld trägt bestimmte Policies
Effect-/Context-Ebene:
D enthält bestimmte Services/Capabilities
Nutzen:
- generische, stark typisierte APIs
- präzise Field-Level-Constraints
- Blueprints und Dependency-Rows
- weniger künstliche Marker-Protocols
11. Verbrauchssemantik gehört nicht in die Typalgebra¶
Verbrauchssemantik (Move, Use-after-Move, Drop) ist nicht Teil der Typalgebra und entsteht nicht über Refinements. Refinements sind monoton-additiv: Sie verfeinern eine Typeigenschaft, sie tragen keine Konsumlogik.
Verbrauchssemantik in JDL entsteht aus Ownership-Labels (own: Unique, !Copyable) plus Bewegungsoperationen und wird vom Typechecker über die TruthProfile-Closure erzwungen.
Diese Mechanik gehört normativ in:
03-runtime.md(Runtime-Modell): Use-After-Move-Diagnose, Move/Drop-Semantik, User-Sicht04a-label-inferenz-regeln.md(Label-Inferenz): Closure-Regeln, die ausown: Unique + Movedie Verbrauchsableitung produzieren
In der Typalgebra wird sie ausdrücklich nicht behandelt.
12. Effect Rows¶
Effect[R, E, D] wird durch Row-Polymorphismus deutlich stärker.
Beispiel:
type UserDeps = {
db: UserDb
log: Logger
}
def fetchUser(id: UserId) -> Effect[User, UserError, UserDeps]
Offene Dependency-Constraints:
Bedeutung:
Algebra:
D1 & D2 kombiniert Dependency-Rows.
D has { field: Service } prüft Mindestanforderungen.
Context C satisfies D, wenn C alle Felder aus D bereitstellt.
Row-Satisfaction wird statisch geprüft¶
Die Prüfung, ob ein Context die Dependency-Row eines Effects erfüllt, findet statisch bei der Effect-Composition statt — also dort, wo Effect und Context zusammengeführt werden, nicht erst bei Ausführung.
Wichtig: Row-Satisfaction prüft Service-Traits/Capability-Typen, nicht konkrete Provider. Wenn ein Effect D has { log: Logger } verlangt, prüft die Composition, ob der Context einen Service-Slot log: Logger besitzt. Welcher konkrete Logger zur Runtime dort gebunden ist (z.B. via Dispatch-Mode oder Plugin-Resolution), ist eine andere Schicht und wird nicht von der Row-Satisfaction abgedeckt. Effect-Composition tut keine Service-Lookups.
Trennung:
D beschreibt benötigte Fähigkeiten.
Context stellt konkrete Fähigkeiten bereit.
Row-Satisfaction prüft das statisch zur Composition-Zeit.
Engine interpretiert oder führt den Effect aus.
Konkreter Provider wird über Dispatch/Plugin gelöst, nicht über die Row.
E bleibt Fehlerkanal und wird über Union-Typen modelliert.
Effect Rows geben Effect[R, E, D] eine natürliche funktionale Richtung, ohne dass JDL dafür ein separates Effekt-Sprachsystem braucht.
13. Opaque, dyn und reified¶
JDL trennt drei Formen von Typinformation:
opaque:
konkreter Typ ist statisch bekannt, aber nach außen verborgen
dyn P:
konkreter Typ ist zur Compile-Zeit nicht bekannt;
Runtime-Typinfo/Protocol-Bindung nötig
reified T:
Typparameter wird explizit zur Laufzeit materialisiert
Diese Trennung ist wichtig:
14. Associated Types¶
Associated Types sind Typplätze innerhalb eines Protocols, die zur Implementierung gehören.
Implementierung:
Associated Types dürfen Constraints haben:
protocol Repository {
type Entity
type Id: Hashable + Equatable
def get(self, id: Self.Id) -> Result[Self.Entity, RepoError]
}
Nutzen:
- weniger generischer Parameterlärm
- klarere API-Semantik
- bessere Abstraktion
- Engine-, Parser-, Generator-, Codec- und Repository-Modelle werden natürlicher
Regel:
Associated Types werden pro provide eindeutig festgelegt.
Sie sind Teil der Protocol-Implementierung.
Sie können Constraints tragen.
15. Variance¶
Variance beschreibt, wie sich ein generischer Typ verhält, wenn sein Typparameter durch einen spezielleren oder allgemeineren Typ ersetzt wird.
Das klingt schlimmer, als es ist. Die einfache Frage lautet:
Wenn A dort verwendbar ist, wo B erwartet wird,
gilt das dann auch für Container[A] und Container[B]?
Oder konkreter:
Die Antwort hängt davon ab, was der generische Typ mit T macht.
Wichtig — Querverweis zur Cast-Effekte-Spec: Die Prämisse "A ist als B verwendbar" ist nicht automatisch erfüllt, nur weil ein Refinement-/Newtype-Verhältnis besteht. Ob ein refinement-verfeinerter Typ wie
Email = str :> ValidEmailstrukturell alsstrgilt — alsoEmail <: strfür Variance-Zwecke — hängt am Cast-System und am Refinement-Subtyping. Die normativen Regeln dazu stehen in07-cast-effekte-refinements.md. Diese Notiz behandelt Variance unter der Annahme, dass die Subtyping-Beziehung bereits durch das Cast-System etabliert ist.
Drei Fälle¶
1. Kovariant: Lesen ist sicher¶
Ein Typ ist kovariant in T, wenn Werte von T nur nach außen gegeben oder gelesen werden.
Beispiel:
Wenn Email als str verwendbar ist, dann kann eine reine Lese-Sequenz von Email auch als Lese-Sequenz von str betrachtet werden.
Warum? Weil nur gelesen wird. Jeder gelesene Email-Wert ist auch ein str.
Typische kovariante Positionen:
- Rückgabewerte
- immutable/read-only Container
- Output-Sequenzen
Result[R, E]inRund meist auchE
2. Kontravariant: Eingabe darf allgemeiner sein¶
Ein Typ ist kontravariant in T, wenn T nur als Eingabe konsumiert wird.
Das typische Beispiel ist eine Funktion:
Eine Funktion, die jeden str akzeptiert, kann auch dort benutzt werden, wo nur Email übergeben wird.
Das ist sicher, weil jede Email auch als str gilt.
Umgekehrt ist es nicht sicher:
val acceptsOnlyEmail: (Email) -> bool = e => ...
val acceptsAnyString: (str) -> bool = acceptsOnlyEmail // Fehler
Eine Funktion, die nur Email korrekt behandeln kann, darf nicht als Funktion für beliebige str gelten.
Merksatz:
Bei Funktionen ist der Parameter kontravariant.
Je allgemeiner die Funktion akzeptiert, desto leichter kann sie eingesetzt werden.
3. Invariant: Keine automatische Ersetzung¶
Ein Typ ist invariant in T, wenn automatische Ersetzung unsicher wäre.
Das wichtigste Beispiel sind mutable Container.
type Email = str :> ValidEmail
val emails: Array[Email] = [...]
val strings: Array[str] = emails // wenn das erlaubt wäre
strings.push("not-an-email") // kaputt
Wenn Array[Email] als Array[str] gelten dürfte, könnte man einen beliebigen str in ein Array schreiben, das eigentlich nur gültige Emails enthalten darf.
Deshalb:
Also:
Funktionsregel¶
Für einen Funktionstyp:
gilt:
Einfach gesagt:
Result¶
Für:
ist die natürliche Regel:
Warum? Beide Typen werden aus Result heraus gelesen. Result konsumiert sie nicht durch Mutation.
Effect¶
Für:
gilt als Ausgangspunkt:
D beschreibt Anforderungen an den Context. Eine Berechnung, die weniger benötigt, kann in einem reicheren Context laufen.
Beispiel:
Ein Effect mit NeedsLog kann in einem Context mit ProdDeps ausgeführt werden, weil ProdDeps mindestens log bereitstellt.
Das ist keine simple Kovarianz oder Kontravarianz. Es ist Row-Satisfaction:
Startregeln für JDL¶
Immutable/read-only Container:
kovariant
Mutable Container:
invariant
Funktionsparameter:
kontravariant
Funktionsrückgaben:
kovariant
Result[R, E]:
R kovariant
E kovariant
Effect[R, E, D]:
R kovariant
E kovariant
D über Row-Entailment
Warum das wichtig ist¶
Variance verhindert, dass Generics mit Refinements, Rows und Funktionstypen unsicher werden.
Ohne klare Regeln passieren Dinge wie:
verfeinerte Werte werden durch allgemeine Schreiboperationen verletzt
Funktionen werden in falscher Richtung substituiert
Effects laufen in Contexts, die ihre Anforderungen nicht erfüllen
Kurz:
Das ist keine akademische Dekoration. Das ist Brandschutz.
16. Zusammenfassung¶
JDLs Typmodell lässt sich auf eine kleine Menge starker Regeln zurückführen:
Record definiert Form.
Alias gibt Namen.
Newtype gibt Identität (immer über das Keyword `newtype`).
Zero-field Newtype gibt materialisierbare Beweis-/Token-Werte ohne Payload.
Tag gibt compile-time Bedeutung ohne Wert.
& kombiniert oder spezialisiert strukturelle Formen (Record-Form-Schicht).
| beschreibt Alternativen.
:> setzt Refinements/Policies (monoton-additiv, keine Verbrauchssemantik).
TypeKind begrenzt gültige Typoperationen.
Row-Polymorphismus macht Struktur generisch.
Effect Rows machen Effekte zu echten algebraischen Bausteinen.
Row-Satisfaction ist statisch zur Effect-Composition.
Associated Types entlasten generische APIs.
Variance hält das Ganze typsicher (in Verbindung mit der Cast-Spec).
Verbrauchssemantik gehört nicht in die Typalgebra (siehe 03-runtime und 04a).
Der entscheidende Entwurfsgrundsatz:
Das verhindert, dass Blueprints zu Structs mit Sonderbehandlung, Aliase zu halben Typen, Tags zu Runtime-Werten oder Effects zu magischen Funktionsaufrufen verkommen.