Zum Inhalt

JDL – Generators und Collectors

Status: Draft
Geltungsbereich: Sprachebene / Stdlib-Oberfläche Namespace: jdl::gen (Stdlib)


Warum Dieses Dokument Existiert

Ohne einheitliches Sequenzmodell driften APIs schnell auseinander (Arrays, Maps, Streams, Cursor jeweils mit eigener Logik).

Generators/Collectors lösen das in JDL über zwei kleine, composable Kernprotokolle.


0. Priorität

Ergänzt:

  • 00-refinements.md
  • 01-grundlagen.md
  • 02-typsystem.md
  • 03-runtime.md

Bei Widerspruch gelten 00 bis 03.


1. Leitidee

  • lazy by default,
  • Pipeline-kompatibel (|>),
  • typsicher erweiterbar über provide,
  • Effekt- und Fehlerlogik bleibt explizit.

2. Kernabstraktionen

2.1 Generator[T]

protocol Generator[T] {
    def next(self) -> Option[T]
}

Bedeutung:

  • liefert sukzessive Some(value) oder None,
  • endlich oder unendlich möglich,
  • Fehlermodell über Elementtyp (z. B. Result[T, E]).

2.1.1 Konstruktible Referenz-Implementierung: PullGen[T]

Generator[T] ist ein Protocol (also ein Vertrag), kein instanziierbarer Typ. Damit Library-Code Generatoren bauen kann, liefert die Stdlib einen kleinen konkreten Referenztyp: PullGen[T].

type PullGen[T]: struct {
    pull: () -> Option[T]
}

provide[T] Generator[T] for PullGen[T] {
    def next(self) -> Option[T] = self.pull()
}

2.1.2 Builder: fromPull und unfold

provide PullGen {
    def fromPull[T](pull: () -> Option[T]) -> PullGen[T] =
        PullGen { pull: pull }

    // State-Machine ohne Coroutines: `step` liefert (value, nextState) oder None.
    def unfold[S, T](state: S, step: S -> Option[(T, S)]) -> PullGen[T] {
        var s = state
        PullGen.fromPull(() => {
            match step(s) {
                | Some((value, next)) => { s = next; Some(value) }
                | None                => None
            }
        })
    }
}

Normativ: PullGen ist der kleinste gemeinsame Nenner für "lazy Sequenzen" in der Stdlib. Optimierte Generatoren (z.B. SliceGen, RangeGen, IoLineGen) können zusätzlich existieren, müssen aber weiterhin Generator[T] implementieren.

2.2 Collect[Src, Out]

protocol Collect[Src, Out] {
    def collect(self: Src) -> Out
}

Bedeutung:

  • trennt lazy Erzeugung von Materialisierung,
  • erlaubt mehrere Zielstrukturen ohne Sondersyntax je Ziel.

3. Standard-Combinators

Combinators sind normale Funktionen, die Generatoren in Generatoren transformieren.

Wichtiges Typdetail (siehe Section 2 / Type System): Generator[T] in Parameterposition ist Sugar für ein implizites Generic G mit Constraint G: Generator[T]. -> Generator[U] bedeutet ein opaquer Rückgabetyp (kein dynamisches Trait-Object).

Konzeptionell:

def filter[T, G](g: G, pred: T -> bool) -> Generator[T]
    where G: Generator[T]

def map[T, U, G](g: G, f: T -> U) -> Generator[U]
    where G: Generator[T]

def mapMany[T, U, G](g: G, f: T -> Generator[U]) -> Generator[U]
    where G: Generator[T]

def filterMap[T, U, G](g: G, f: T -> Option[U]) -> Generator[U]
    where G: Generator[T]

In der Stdlib werden diese typischerweise als Wrapper-Generatoren implementiert (z.B. MapGen[G, T, U]) oder via PullGen.unfold(...).

Kernnutzen: große Verarbeitungspfade bleiben als kleine, prüfbare Schritte lesbar.


4. Materialisierung und Sugar

  • |> [] = Sammeln in Sequenzcontainer,
  • |> {} = Sammeln in Mapping-Container.

Konzeptionelles Desugaring:

Collect[Generator[T], [T]].collect(g)
Collect[Generator[(K, V)], Map[K, V]].collect(g)

Die konkrete Instanzwahl erfolgt über Typkontext.


5. Fehlerintegration

Beispiel „fail-fast sammeln":

provide[T, E] Collect[Generator[Result[T, E]], Result[[T], E]] {
    def collect(self: Generator[Result[T, E]]) -> Result[[T], E] { ... }
}

Wichtig: Fehlersemantik steckt im Typ/Collector, nicht im Basis-Generator-Protokoll.


6. Effektintegration

Generatoren dürfen effektvolle Quellen kapseln (z. B. Dateilesen), aber Abhängigkeiten bleiben Funktionseigenschaft — als expliziter Parameter (imperativer Stil) oder über Effect[R, E, D] im Rückgabetyp (Effect-Stil). Sie sind kein verstecktes Protokollverhalten.

// Imperativer Stil – Service als Parameter:
def grep(fs: Fs, path: Path, needle: str) -> [str] =
    readLines(fs, path)
    |> filter(line => line.contains(needle))
    |> []

// Effect-Stil – lazy, komponierbar:
def grep(path: Path, needle: str) -> Effect[[str], FsError, Fs] =
    readLines(path)
    |> filter(line => line.contains(needle))
    |> []

7. Normative Invarianten

  1. Generator.next liefert Option[T].
  2. Materialisierung läuft über Collect[Src, Out].
  3. Sugar ist reines Desugaring auf Collect.
  4. Fehlerpolitik wird explizit über Typen/Collector-Instanzen modelliert.
  5. Effekte bleiben sichtbar — über Effect[R, E, D] im Rückgabetyp oder als explizite Service-Parameter. deps als Konstrukt existiert nicht.

8. Offene Punkte (Phase 2)

  • weitere Collector-Ziele (Set, SmallArray, spezialisierte Buffer),
  • Optimierungen (Fusion, Inlining, Allocation-Strategien),
  • optionale Comprehension-Syntax als rein additive Sugar-Schicht.