JDL — Partielle Applikation¶
Status: Konsolidierungsentwurf, Arbeitsstand
Version: 0.1.3
Datum: 2026-10-02
Geltungsbereich: Funktionen teilweise aufrufen — zur Laufzeit mit _, in der Meta-Phase mit
Call(f) — und was dabei mit Typ, Row, Fehlern und Blueprints geschieht
Quellen: Entscheidungen aus den Arbeitsgesprächen (Stand 2026-10-02),
jdl-blueprint-spec 0.5.2 §3.5, §3.6, §3.7, §5.4, §8.1, §9.1, jdl-meta-phase 0.3.1,
jade-compiler-db-spec §9.3, jdl-effect-rows-und-throw, wie-jade-uebersetzt §4.3, §4.4, §4.9
0. Über dieses Dokument¶
0.1 Kennzeichnung¶
| Marke | Bedeutung |
|---|---|
[N] |
bestehend normativ belegt, Fundstelle genannt |
[E] |
entschieden, aber noch in keinem Quelldokument ausgeschrieben |
[A] |
aus bestehendem Material ableitbar, aber nirgends ausgeschrieben |
[V] |
Vorschlag, ausgearbeitet, aber nicht entschieden |
[O] |
offen |
Dieses Dokument ist neu. Fast alles darin trägt [E], weil es in Arbeitsgesprächen
entschieden, aber bisher nirgends aufgeschrieben wurde.
0.2 Syntax¶
Alle Beispiele zeigen Form, nicht Anwendung, und tragen // spekulativ. Entschieden ist nur
das, was im Text mit [E] steht — etwa dass _ die Lücke markiert —, nicht die übrige
Schreibweise der Beispiele.
Durchgehendes Beispiel. Eine Funktion, die eine E-Mail verschickt:
sendhat drei Parameter.- In der Row stehen der Service
Mailerund ein möglicher FehlerMailError. - Die Funktion gibt nichts zurück (
Unit).
0.3 Herkunft¶
Zu diesem Thema gab es in den Arbeitsgesprächen eine formale Notation mit Regeln für die
Laufzeitform, die Meta-Form und ihre Kohärenz. Sie ist nicht als Dokument erhalten. Die Regeln
in §6 sind aus den festgehaltenen Entscheidungen neu formuliert, nicht abgeschrieben. Eine
formale Fassung nach jdl-formale-notation-style-guide steht aus (§5).
1. Worum es geht¶
Eine Funktion partiell anwenden heißt: einige Argumente jetzt festlegen, die übrigen später liefern. Heraus kommt eine neue Funktion, die nur noch die offenen Parameter erwartet.
Das braucht man, wenn eine Funktion an einer Stelle vorbereitet und an einer anderen aufgerufen wird: ein Versand, dessen Betreff feststeht, dessen Empfänger aber erst in einer Schleife bekannt wird.
JDL bietet dafür einen Gedanken in zwei Phasen:
Laufzeit send(to = _, subject = "Hallo", body = _) ergibt eine Funktion
Meta-Phase Call(send) & { subject = "Hallo" } ergibt einen Blueprint
Beide bedeuten dasselbe (§4). Die Laufzeitform ist die schnelle Schreibweise im Code, die Meta-Form die Schreibweise in Blueprints, wo sich die festgelegten Werte per Profil anpassen lassen.
Was es nicht gibt: Currying [E]. Eine Funktion mit drei Parametern ist nicht
automatisch eine Kette aus drei Funktionen mit je einem Parameter. Partielle Applikation
passiert nur dort, wo jemand sie mit _ ausdrücklich verlangt.
2. Zur Laufzeit: die Lücke _¶
2.1 Woran man sie erkennt [E]¶
Steht in einem Aufruf ein _ an der Stelle eines Arguments, ist der Aufruf eine partielle
Applikation. Ohne _ ist er ein gewöhnlicher Aufruf.
// spekulativ
val notify = send(to = _, subject = "Wochenrückblick", body = news::weekly)
notify(to = "anna@example.com")
- Zeile 2:
tobleibt offen,subjectundbodywerden festgelegt.notifyist eine Funktion. - Zeile 3:
notifywird mit dem fehlenden Argument aufgerufen. Erst jetzt wird die E-Mail verschickt.
Ein fehlendes Argument ist ein Fehler [E]. Wer ein Argument weglässt, ohne _ zu
schreiben, bekommt keine partielle Applikation, sondern einen Compilerfehler. Die Lücke muss
man sehen können.
Das ist dieselbe Regel wie überall in Jade: kein Automatismus, der rät, was gemeint war.
2.2 Die Lücke gehört zum innersten Aufruf [E]¶
Stehen Aufrufe ineinander, gehört ein _ zu dem Aufruf, der ihn unmittelbar umschließt.
_steht informat(...).formatwird partiell angewendet und ergibt eine Funktion.printbekommt diese Funktion, nicht einen formatierten Text.
Wer „print soll später aufgerufen werden“ meint, muss die Lücke in print setzen. Die Regel
ist bewusst die einfachste denkbare; sie macht den Ausdruck von innen nach außen lesbar.
2.3 Festgelegte Argumente werden sofort ausgewertet [E]¶
Ein festgelegtes Argument wird ausgewertet, wenn die partielle Applikation entsteht, nicht erst beim späteren Aufruf.
In §2.1 wird news::weekly also einmal ausgewertet, wenn notify entsteht. Ruft man notify
hundertmal auf, wird es nicht hundertmal neu berechnet.
Das macht das Verhalten vorhersagbar: Was festgelegt ist, steht fest.
2.4 Row und Fehler wandern in den Ergebnistyp [E]¶
Die partielle Applikation selbst führt nichts aus. Die Row der Funktion geht deshalb unverändert in den Typ des Ergebnisses über:
send (to: str, subject: str, body: Html) ->{Mailer, Throw[MailError]} Unit
notify (to: str) ->{Mailer, Throw[MailError]} Unit
- Die festgelegten Parameter verschwinden aus der Parameterliste.
- Die Row bleibt vollständig. Wer
notifyaufruft, brauchtMailerund muss mitMailErrorrechnen — genau wie beim Aufruf vonsend.
Nichts verschwindet heimlich. Man sieht an der Signatur des Ergebnisses, was der spätere Aufruf braucht.
Fehler beim Festlegen selbst gehören dagegen zur Stelle, an der die Applikation entsteht.
Wirft die Auswertung eines festgelegten Arguments (§2.3), trägt die umgebende Funktion diesen
Fehler in ihrer eigenen Row [A].
2.5 Was man nicht festlegen kann [E]¶
Geliehene Werte. Ein geliehener Wert darf die Stelle nicht überleben, an der er geliehen
wurde. Eine partielle Applikation kann aber länger leben. Einen geliehenen Wert festzulegen,
ist deshalb ein Fehler. Wer den Wert festlegen will, übergibt eine Kopie (.clone()).
var-Parameter. Ein var-Parameter gibt der Funktion Schreibzugriff auf eine Bindung des
Aufrufers. Diese Bindung gibt es erst beim eigentlichen Aufruf. var-Parameter bleiben deshalb
immer offen und müssen mit _ stehen.
2.6 Namen bleiben erhalten [E]¶
Die offenen Parameter behalten ihre Namen. notify erwartet to, nicht „das erste
Argument“.
Benannte Argumente sind bei partieller Applikation die bevorzugte Schreibweise, weil sonst schwer zu sehen ist, welche Stelle offen bleibt.
[O]Ob benannte Argumente ab zwei Parametern Pflicht werden, ist offen.
2.7 Festgelegte Werte gehören nicht zum Typ [E]¶
Zwei partielle Applikationen von send mit verschiedenem Betreff haben denselben Typ. Welche
Werte festgelegt sind, ist Inhalt, nicht Typ.
Wer einer solchen Funktionsform einen Namen geben will, schreibt einen Typalias:
Notifierist nur ein anderer Name für diese Signatur.notifyaus §2.1 ist einNotifier, ebenso jede andere Funktion mit dieser Signatur.
3. In der Meta-Phase: Call(f)¶
3.1 Was Call baut [E]¶
Call ist eine Meta-Funktion. Sie nimmt eine Funktion und baut daraus einen Blueprint: eine
Beschreibung des Aufrufs, in der jeder Parameter ein Feld ist.
Call(send)baut in der Meta-Phase einen Blueprint aus der Signatur vonsend.& { ... }ist ein gewöhnliches Blueprint-Profil (jdl-blueprint-spec§3.6). Es legtsubjectfest.toundbodybleiben offen.
Anders als in §2.1 wird body hier nicht festgelegt. Der Inhalt eines Wochenrückblicks
entsteht erst zur Laufzeit. In der Meta-Form würde er beim Übersetzen ausgewertet, und jede
Woche ginge derselbe Rückblick hinaus (§4).
Der gebaute Blueprint hat:
- ein Feld
target, das aufsendverweist, - für jeden Parameter ein Feld mit demselben Namen. Es ist ein
varmit dem Vorgabewert „offen“, sodass ein Profil es festlegen kann. var-Parameter der Funktion werden zuval-Feldern mit dem Wert „offen“. Kein Profil kann sie festlegen — dieselbe Regel wie §2.5.
Welchen Typ ein solches Feld hat, um „offen oder ein Wert“ auszudrücken, ist in den
Arbeitsgesprächen als Slot[T] skizziert, aber nicht festgelegt (§5).
3.2 Die Signatur von Call [E]¶
Metaam Pfeil: Die Funktion läuft beim Übersetzen (jdl-meta-phase§2.1).CompilerDb: Sie liest die Signatur vonfaus der CompilerDB.Throw[DiagnosticSet]: Sie kann mit Diagnosen scheitern.- Sie liefert einen Patch für einen neuen Typ, keinen fertigen Typ und keinen Wert.
Call schreibt nicht selbst [E]. Wie jede Meta-Funktion darf sie die CompilerDB nur über
Patches ändern (jdl-meta-phase §4.7, MP-8). Ihr Ergebnis ist eine Änderungsabsicht: „lege
diesen Blueprint-Typ an“. Den Weg gibt es schon; db.createStruct liefert dieselbe Art von
Patch (jade-compiler-db-spec §9.3, dort noch unter dem Namen TypeDescriptorPatch).
Den Namen gibt die Deklaration: In type WeeklyNews : Call(send) & { ... } heißt der neue Typ
WeeklyNews. Erst der Materializer prüft den Patch und legt den Typ an.
Eine Meta-Funktion, die einen Typ erzeugt, steht damit dort, wo sonst ein Typ steht. Das ist gewollt: Der Typ entsteht durch eine gewöhnliche Funktion, nicht durch ein eigenes Sprachmittel.
3.3 Die CallEngine [E]¶
Den Blueprint wertet eine CallEngine aus, wie jede andere Engine (jdl-blueprint-spec §9).
Dass describe den CallDescriptor als getypten Wert liefert, folgt der Form der RuntimeEngine in
jdl-blueprint-spec §9.1; diese Form ist dort noch ein Vorschlag [V].
describe Blueprint -> CallDescriptor in der CompilerDB
instantiate Excerpt[CallDescriptor] -> die verbleibende Funktion zur Laufzeit
Der CallDescriptor hält vier Dinge:
target Verweis auf die Funktion
fixed die festgelegten Argumente
open die offenen Parameter mit Namen und Typ
row die Row der Funktion
target ist ein gewöhnlicher Verweis [E]. Innerhalb des Artefakts zeigt er wie jeder
Verweis auf den Excerpt-Hash seines Ziels (wie-jade-uebersetzt §4.4), also auf die Funktion
samt Rumpf. Ändert sich der Rumpf von send, ändert sich dieser Hash und mit ihm der Hash des
CallDescriptors. Das ist richtig so: Der Excerpt zeigt dann wirklich auf anderen Code. Über
Artefaktgrenzen gilt stattdessen §3.4.
Er ist serialisierbar, wie jeder Descriptor. Die festgelegten Werte müssen es dafür ebenfalls
sein (jdl-blueprint-spec §7.2). Den Descriptor selbst gibt es nur in der CompilerDB;
instantiate bekommt seinen Excerpt (jdl-blueprint-spec §9.1, BP-22). Für einen Call
überleben alle vier Felder die Projektion, weil instantiate jedes davon braucht.
instantiate liefert eine Funktion mit genau den offenen Parametern und der vollständigen Row
— denselben Typ, den die Laufzeitform aus §2.4 liefern würde.
3.4 Was Call annimmt [E]¶
Keine Meta-Funktionen. Eine Funktion mit Meta am Pfeil kann nicht an Call übergeben
werden. Ein Blueprint beschreibt etwas, das zur Laufzeit geschieht, und eine Meta-Funktion gibt
es zur Laufzeit nicht (jdl-meta-phase §2.2).
Trusted Core: Call darf aus der CompilerDB nur die Signatur von f lesen, nie den
Rumpf. Dann hängt die Form des gebauten Blueprints — seine Felder und ihre Typen — nur von der
Signatur ab. Ändert sich das Innere von send, bleibt WeeklyNews dem Namen und der Form nach
derselbe Typ. Sein Hash ändert sich trotzdem, weil target auf den neuen Rumpf verweist (§3.3).
Neu geprüft werden muss deshalb nur, was mehr als die Form liest. Läse Call den Rumpf, hinge
an jeder Implementierungsänderung eine neue Typprüfung aller Benutzer.
Einen Verweis auf send zu setzen, heißt nicht, seinen Rumpf zu lesen: Call übernimmt den
Hash, ohne den Inhalt dahinter auszuwerten.
Aufrufe über Artefaktgrenzen [E]. Soll ein durch Call(f) beschriebener Aufruf
außerhalb des eigenen Artefakts ausgeführt werden, muss f über eine exportierte Schnittstelle
erreichbar sein. Der Zielverweis wird bei der Zulassung über Name und Schnittstellen-Hash geprüft
(wie-jade-uebersetzt §4.9). Der Hash umfasst die Signatur einschließlich Row; er ist der
Excerpt-Hash des Exports, nicht der der Funktion samt Rumpf.
Gebunden wird die am Ziel installierte Implementierung dieser Schnittstelle: der Rumpf, der im
Ziel-Artefakt auf diesen Export verweist (wie-jade-uebersetzt §4.9). Der Verweis garantiert
Schnittstellenidentität, nicht Implementierungsidentität.
Call(f)
|
+-- lokal -----------------> die konkrete Funktion f
|
+-- über Artefaktgrenze
|
v
exportierte Schnittstelle
|
+--> Name
|
+--> Schnittstellen-Hash, einschließlich Row
|
v
Zulassung im Ziel-Artefakt
|
v
dort installierte Implementierung
target im CallDescriptor sagt über eine Artefaktgrenze also nicht „führe genau diesen Code
aus“, sondern „führe eine zugelassene Implementierung dieser Schnittstelle aus“. Das folgt aus
der allgemeinen Regel für Verweise über Artefaktgrenzen: Sie zeigen nur auf Schnittstellen
(wie-jade-uebersetzt §4.9). Die Implementierung gehört dort nicht zur Identität des
beschriebenen Aufrufs.
Ist f nicht exportiert, kann der Aufruf nur innerhalb seines Artefakts ausgeführt werden.
3.5 Verwendung in einem Context [E]¶
Ein Call-Blueprint kann Feld eines anderen Blueprints sein, etwa eines Contexts. Die
Funktion, die ihn benutzt, nennt ihn in ihrer Pipe-Liste, und ihre Row muss die Row des
Aufrufs mittragen:
// spekulativ
type NewsletterContext : blueprint {
services = [Mailer, Subscribers]
var weekly: WeeklyNews
var run: | .services.*, .weekly | (args: [str]) ->{Subscribers, Mailer, Throw[MailError]} ExitCode
}
- Zeile 3: Der Context registriert zwei Services.
- Zeile 4:
weeklyist ein Feld vom TypWeeklyNews, dem vorbereiteten Versand aus §3.1. Damit ist es ein verschachtelter Blueprint (jdl-blueprint-spec§3.7). Ob es ohne=ein Pflichtfeld ist, ist dort offen (Punkt 22). - Zeile 5:
runbietet alle Services an und nimmt das Feldweeklymit (jdl-blueprint-spec§5.4). Die Row nenntSubscribers, weilrundie Abonnenten liest, undMailersamtMailError, weil ein Aufruf von.weeklyam Endesendaufruft.
Was run von weekly bekommt [E]. Weil weekly ein verschachtelter Blueprint ist, wird
in run kein Wert eingebacken, sondern ein Verweis auf den Excerpt seines CallDescriptors. Zur
Laufzeit steht dort die Funktion, die das instantiate der CallEngine geliefert hat
(jdl-blueprint-spec §3.7). run ruft sie mit den offenen Argumenten to und body auf.
Das ist zulässig, weil die Funktion keine Autorität trägt: Sie braucht Mailer beim Aufruf, und
den muss run in seiner Row haben. Ein Blueprint, dessen instantiate ein Handle liefert,
dürfte so nicht in der Pipe-Liste stehen (jdl-blueprint-spec §3.7).
Fehlte Mailer in der Row, wäre das ein Fehler: Die Row von weekly wandert beim Aufruf in
die Row von run (§2.4), und run hätte sie nicht angegeben.
4. Kohärenz [E]¶
Laufzeitform und Meta-Form sind derselbe Gedanke und müssen dasselbe Ergebnis liefern:
send(to = _, subject = s, body = b)
ergibt denselben Typ und dasselbe Verhalten wie
instantiate auf dem Excerpt von describe(Call(send) & { subject = s, body = b })
Das heißt konkret:
- gleiche offene Parameter, mit gleichen Namen und Typen,
- gleiche Row,
- gleiche Regeln für geliehene Werte und
var-Parameter (§2.5).
Ein Unterschied bleibt und ist gewollt: Die Laufzeitform wertet festgelegte Argumente zur Laufzeit aus (§2.3), die Meta-Form beim Übersetzen. In der Meta-Form können deshalb nur Werte festgelegt werden, die in der Meta-Phase feststehen und serialisierbar sind.
5. Offene Punkte¶
| # | Punkt | Abschnitt |
|---|---|---|
| 1 | Benannte Argumente ab zwei Parametern Pflicht? | §2.6 |
| 2 | Call(send) oder Call[send] |
§3.1 |
| 3 | Typ eines offenen Feldes (Slot[T]) und seine Umwandlung |
§3.1 |
| 4 | Formale Fassung der Regeln nach dem Style Guide | §6 |
| 5 | Verhältnis zu einem verbrauchenden Parametermodus | §2.5 |
| 6 | Wie jeder durch Call erzeugte Typ an die CallEngine gebunden wird |
§3.2, §3.3 |
Punkt 2. Die Tendenz geht zu runden Klammern. Die Regel dahinter wäre: Runde Klammern beim
Aufruf genau dann, wenn sie auch bei der Deklaration stehen — Call ist eine Funktion und wird
deshalb wie eine aufgerufen. Das Vorbild ist Zig, wo ArrayList(i32) ein Typ ist, der durch
einen Funktionsaufruf entsteht. Eckige Klammern blieben Typparametern vorbehalten.
Entschieden ist das nicht; dieselbe Frage stellt sich bei Refinements wie ExtensibleVia(...).
Punkt 3. Das Feld muss zwei Zustände ausdrücken: offen oder festgelegt mit einem Wert vom
Typ T. Wie ein festgelegter Wert in ein Slot[T]-Feld kommt — ob subject = "Hallo" direkt
geht oder eine Umwandlung braucht —, ist offen. Für Anfänger sollte es direkt gehen.
Punkt 5. Ein Parametermodus, der einen Wert verbraucht statt ihn zu leihen, ist als offene Sprachfrage geführt. Kommt er, muss feststehen, ob ein verbrauchter Wert festgelegt werden kann. Naheliegend ist: ja, weil die partielle Applikation ihn dann besitzt.
Punkt 6. Jedes Call(f) erzeugt einen neuen Typ, und für jeden Blueprint-Typ gibt es
höchstens eine Engine (jdl-blueprint-spec §9.4). Wie das provide der CallEngine an jeden
dieser Typen kommt, steht nirgends. Naheliegend: Der Patch von Call trägt diese Bindung mit.
6. Normative Regeln¶
PA-1 Ein Aufruf mit mindestens einem `_` an Argumentstelle ist eine
partielle Applikation. Ohne `_` ist er ein gewöhnlicher Aufruf;
ein fehlendes Argument ist ein Compilerfehler.
[E]
PA-2 Ein `_` gehört zum innersten ihn umschließenden Aufruf.
[E]
PA-3 Festgelegte Argumente werden ausgewertet, wenn die partielle
Applikation entsteht.
[E]
PA-4 Das Ergebnis ist eine Funktion mit den offenen Parametern in
ursprünglicher Reihenfolge, mit ihren Namen, und mit der
unveränderten Row der angewendeten Funktion.
[E] / [A] für die Reihenfolge
PA-5 Geliehene Werte können nicht festgelegt werden. `var`-Parameter
bleiben immer offen.
[E]
PA-6 Festgelegte Werte sind nicht Teil des Typs.
[E]
PA-7 Es gibt kein automatisches Currying.
[E]
CALL-1 `Call` ist eine Meta-Funktion. Sie liefert aus der Signatur
einer Funktion einen Patch für einen Blueprint-Typ mit einem Feld
`target` und einem Feld je Parameter. Parameter werden zu
`var`-Feldern mit Vorgabe „offen“, `var`-Parameter zu `val`-Feldern
mit Wert „offen“. Den Namen gibt die Deklaration, angelegt wird
der Typ vom Materializer.
[E]
CALL-2 `Call` liest nur die Signatur der Funktion, nie ihren Rumpf.
Funktionen mit `Meta` am Pfeil sind nicht zulässig.
[E]
CALL-3 Die CallEngine erzeugt einen serialisierbaren CallDescriptor aus
target, fixed, open und row. instantiate bekommt dessen Excerpt
und liefert die verbleibende Funktion. target ist ein
gewöhnlicher Verweis über den Excerpt-Hash des Ziels.
[E] / [V] für die getypte Form von describe
CALL-4 Laufzeitform und Meta-Form sind kohärent: gleiche offene
Parameter, gleiche Row, gleiche Regeln nach PA-5. In der
Meta-Form sind nur Werte festlegbar, die in der Meta-Phase
feststehen und serialisierbar sind.
[E]
CALL-5 Über Artefaktgrenzen darf ein Call nur eine exportierte
Schnittstelle treffen. Geprüft wird bei der Zulassung über Name
und Schnittstellen-Hash einschließlich Row. Gebunden wird die am Ziel
installierte Implementierung; garantiert ist Schnittstellen-,
nicht Implementierungsidentität.
[E]
Änderungsprotokoll¶
0.1.3 (2026-10-02) — Kohärenzprüfung. §1: Laufzeitform der Gegenüberstellung lässt body
offen wie die Meta-Form. §3.1: WeeklyNews legt nur subject fest; ein Wochenrückblick als
festgelegter Wert hätte beim Übersetzen festgestanden. §3.2: Call liefert ein
TypePatch, den Namen gibt die Deklaration, angelegt wird über den Materializer;
CALL-1 angepasst. §3.3: target ist ein gewöhnlicher Verweis über den Excerpt-Hash; Hinweis,
dass die getypte Form von describe in der Blueprint-Spec ein Vorschlag ist; CALL-3 angepasst.
§3.4: Begründung für „nur die Signatur lesen“ auf die Form des Blueprints umgestellt; der Hash
von WeeklyNews ändert sich mit target, die Form nicht. „Schnittstellen-Hash“ statt
„Signatur-Hash“, als Excerpt-Hash des Exports; CALL-5 angepasst. Neuer offener Punkt 6.
§3.5: var weekly: WeeklyNews statt :=
(:= nur für Werte); was run von einem verschachtelten Blueprint bekommt. §3.4: Die
installierte Implementierung ist der Rumpf, der im Ziel-Artefakt auf den Export verweist. §3.5:
Warum die Funktion aus weekly über die Pipe-Liste kommen darf: Sie trägt keine Autorität.
TypeDescriptorPatch heißt TypePatch (Nachtrag A8).
0.1.2 (2026-10-02) — §3.3, §4 und CALL-3: instantiate bekommt den Excerpt des
CallDescriptors, nicht den Descriptor selbst, entsprechend jdl-blueprint-spec 0.5.1.
0.1.1 (2026-09-29) — §3.4 um Aufrufe über Artefaktgrenzen ergänzt: nur über exportierte Schnittstellen, geprüft über Name und Signatur-Hash, garantiert Schnittstellen- statt Implementierungsidentität. Regel CALL-5.
0.1.0 (2026-09-29) — Erste Fassung. Entscheidungen aus den Arbeitsgesprächen zur partiellen
Applikation zur Laufzeit (_) und in der Meta-Phase (Call) erstmals aufgeschrieben. Die
formalen Regeln aus den Gesprächen sind nicht erhalten und hier neu formuliert; eine formale
Fassung steht aus. Das Beispiel NewsletterContext in §3.5 führt die vollständige Row
->{Subscribers, Mailer, Throw[MailError]}; eine frühere Skizze im Gespräch führte nur
Throw[MailError] und war damit unvollständig.