Ein kleiner Überblick über 􊨖lgola

􊨖lgola ist, wie der Name vielleicht schon vermuten lässt, eine syntaktisch an Algol angelehnte Sprache. Das bedeutet unter anderem, dass Blöcke durch Schlüsselwörter eingerahmt werden und nicht, wie es der eine oder andere eher gewohnt sein mag, durch geschweifte Klammern. Letztere sind ausschließlich der Darstellung von Mengen vorbehalten, was schon relativ viel über 􊨖lgola aussagt.

Im Gegensatz zu den meisten, auch neueren Programmiersprachen macht 􊨖lgola auch wirklich Gebrauch von den Möglichkeiten, die durch den Unicode-Zeichensatz gegeben sind. Beispielsweise werden Objekte erwartungsgemäß mit auf Ungleichheit geprüft, und nicht etwa durch eine Kombination wie !=, /= oder was auch immer sich die Sprachdesigner als am passendsten aus dem ASCII-Zeichensatz zusammengebastelt haben.

So verfügt 􊨖lgola auch über Mechanismen, die man meist schon aus dem Mathematikunterricht in der Schule kennt. Die Elementbeziehung zu einer Menge kann dementsprechend mit ∈ geprüft werden, wer Summen bilden möchte, kann dies etwa über ∑ tun, und wer je eine Abbildung definiert hat, fühlt sich mit ↦ sicher wie Zuhause. Alles andere muss man ohnehin erst lernen. Ein Programmierer C-orientierter Sprachen wird sich anfangs ein bisschen umgewöhnen müssen, aber alle anderen haben es hoffentlich ein bisschen einfacher.

Hallo Welt

Traditionell beginnt eine Einfürung in eine Programmiersprache mit einem Gruß an die Welt:

program HelloWorld is
override
method run() → () is
printLine(“Hello World!”)
end
end

Das ist allerdings suboptimal, denn diese Applikation – so klein sie auch sein mag – ist nicht internationalisierbar. Korrekt müsste es also eher

program HelloWorld is
message HELLO_WORLD en “Hello World!”
override
method run() → () is
printLine(HELLO_WORLD)
end
end

lauten. Dabei gibt das „en“ vor der Zeichenkette an, dass diese schon den korrekten Text für die Sprache Englisch enthält. Damit kann schon zur Übersetzungszeit geprüft werden, ob die Texte für die Internationalisierung (vollständig) definiert sind.

Da unter 􊨖lgola alles ein Objekt ist, gilt dies natürlich auch für Programme. Ein Programm ist demnach ein Objekt, zu dessem Lebenszyklus der Aufruf der run-Methode gehört. Diese Methode kann überschrieben und entsprechend nach eigenen Wünschen implementiert werden; hier mir der grüßenden Ausgabe auf das startende Terminal oder, wenn nicht gegeben, auf ein eigens für diesen Zweck angezeigtes Systemfenster.

Wer eine main-Methode vermisst, der muss nicht enttäuscht sein: die gibt es immer noch, spielt allerdings für „normale“ 􊨖lgola-Programme praktisch keine Rolle. Abgesehen davon hat ein 􊨖lgola-Programm viele Vorteile. Soll etwa eine Person gegrüßt werden, deren Name als Argument übergeben wird, so kann das recht einfach über die Annotation einer Instanzvariablen bewerkstelligt werden.

program HelloWorld is
message HELLO_WORLD en “Hello \{1:𝕊}!”
argument
name : 𝕊
method run() → () is
printLine(HELLO_WORLD, name)
end
end

Das daraus resultierende Programm gewährleistet, dass dieses nur dann gestartet wird, wenn beim Programmaufruf genau ein Argument angegeben wurde.

▸ aalgola HelloWorld Error: Missing required argument “name”! — Usage: aalgola HelloWorld ›name‹ ▸ aalgola HelloWorld Jupiter Hello Jupiter!

Unter bestimmten Umständen ist übrigens ein Programm-Rahmen für die Ausgabe gar nicht nötig. Beispielsweise kann – als Skript ausgeführt – auf das ganze Drumherum verzichtet werden, da in diesem Fall die Quelle einfach der Reihe nach abgearbeitet wird.

(Block-)Anweisungen, Kommandos und Ausdrücke

Wie dem HelloWorld-Beispiel vielleicht schon anzusehen war, gibt es aus syntaktischer Sicht nur drei große Varianten, aus der eine 􊨖lgola-Quelle aufgebaut wird:

Variablen und Typen

Variablen, Unveränderliche und Konstanten

Eines der wichtigsten Hilfsmittel sind Variablen, die je nach Geschmack mehr oder weniger gut wiederbeschrieben werden können. Jede Variable hat während ihrer Lebensdauer einen bestimmten Typ, der sich nach der ersten Festlegung nicht mehr ändern kann.

s :← “abc”Eine Zeichenkette
n :← 1000Eine ganze Zahl
p :← Person()Eine Person

Dabei erlaubt der Pfeil eine erneute Zuweisung, derweil ein Gleichheitszeichen eine Unveränderlichkeit der Variablen anzeigt.

n :← 1
n ← 2
c := 1
c ← 2↯ nicht möglich!

Bei der Verwendung der beiden Zuweisungsoperatoren Pfeil und Gleichheitszeichen ist genau darauf zu achten, was man erreichen möchte. Die Verwendung des Gleichheitszeichens impliziert tatsächlich die Gleichheit und damit die Unveränderlichkeit der „Variablen“. Für den Übersetzer bedeutet dies, dass überall dort wo eine solche Unveränderliche eingesetzt wird, auch deren Wert stehen darf und die dazugehörige „Variable“ möglicherweise gar nicht im Programmcode existiert.

Darüber hinaus kann übrigens eine Unveränderliche auch inhaltlich nicht geändert werden. Referenziert eine Unveränderliche ein veränderliches Objekt, so können keine Methoden aufgerufen werden, die den Zustand des Objekts verändern.

p := Person(…)
p.modifyingMethod()↯ nicht möglich!
v :← p.readingMethod()Möglich.

Außerhalb von Blöcken, etwa im Zusammenhang mit Objektvariablen, müssen diese wunschgemäß annotiert werden. Ist eine Objektvariable immutable, so kann sie – wie oben erwähnt – nach der Initialisierung nicht mehr verändert werden. Ist eine Objektvariable hinaus const so handelt es sich darüber hinaus um eine echte Konstante. Eine Konstante ist – wie der Name schon sagt – auch über die Lebensdauer eines Programms hinaus unveränderlich. Die Lichtgeschwindigkeit etwa ist konstant. Der Mehrwertsteuersatz dahingegen ist zwar nicht veränderlich aber nur bis zur nächsten Gesetzesänderung konstant.

Technisch gesehen bedeutet das, dass fremde Quellen den Wert zum Zeitpunkt der Übersetzung in den ausführbaren Code übernehmen können. Dies bedeutet insbesondere, dass bei einer Veränderung einer Konstanten sicher gestellt werden muss, dass alle davon abhängige Quellen erneut übersetzt werden müssen.

Allerdings gibt es auch Naturkonstanten, die ihrer Natur nach zwar konstant sind, aber deren Wert empirisch ermittelt wird und sich dementsprechend verändern kann. In diesem Fall kann über die zusätzliche Angabe der Annotation immutable sicher gestellt werden, dass diese Abhängigkeit (mit den entsprechenden Performanzeinbußen) erst zur Laufzeit aufgelöst wird.

Typen

Obwohl oben der Typ einer Variablen durch den Typ des zugewiesenen Objektes bestimmt wurde, kann dieser natürlich auch explizit angegeben werden.

x :← 1Vermutlich ℕ₃₂ (über Inferenz ermittelt).
y : ₃₂ ← 1ℤ₃₂ (explizit angegeben).
z :← 1.0𝔽₆₄ (über Inferenz ermittelt).
a : 𝔸₇ ← ‘A𝔸₇ (explizit angegeben).
s :← “abc”𝕊 (über Inferenz ermittelt).

Standardtypen

Boole’sche Werte

Neben den „normalen“ Boole’schen Werten mit true und false unterstützt 􊨖lgola standardmäßig auch drei- und vierwertige Logik. Die dreiwertige Logik 𝔹₃ hat zusätzlich zu true und false den Wert uncertain (unsicher), und die vierwertige Logik 𝔹₄ hat darüber hinaus noch den Wert impossible (unmöglich).

Ganze Zahlen

Die ganzen Zahlen sind entweder nur durch den Speicherplatz der Maschine begrenzt (ℕ und ℤ) oder durch die Registerbreite k. Ein k kann deshalb einen Wert von −2k − 1 bis 2k − 1 − 1 annehmen. Bei ℕ handelt es sich um die natürlichen, also nicht negativen Zahlen. Tatsächlich versteht sich ℕ als ein ℤ, bei dem nur nicht negative Werte zugelassen sind. Das ist nicht mit einer vorzeichenlosen Zahl zu verwechseln, die nicht nur eine andere Repräsentation sondern auch ein unterschiedliches Überlaufverhalten hat: kk{x ≥ 0}. Dementsprechend kann ein k nur Werte von 0 bis 2k − 1 − 1 annehmen.

Im Gegensatz zu vielen anderen Programmiersprachen werden Überläufe nicht geduldet und mit einer Ausnahme geahndet. Für diejenigen, die wissen, was sie tun, gibt es die „Quadratoperatoren“ wie ⊞, ⊟, ⊡ und ⧄ die eventuelle Überläufe ignorieren.

x : ₃₂ ← 2³⁰1 073 741 824
y : ₃₂ ← 2³⁰1 073 741 824
assert x + y throws Overflow
assert xy = −2³¹ = −2 147 483 648

Unter Java nämlich wurde bei der Implementierung einer binären Suche der Mittelwerts von x und y mit Hilfe von

(x + y) / 2

berechnet. Erst über zehn Jahre nach der Implementierung fiel auf, dass dies bei großen x und y zu Fehlern führt. Nur weil Java glücklicherweise eine Bereichsprüfung bei Array-Zugriffen macht, ist dieser Fehler überhaupt aufgefallen; bei C-orientierten Sprachen, die auf diesen Luxus verzichten, hätte die Fehlerursache lange unentdeckt und fatale Folgen haben können.

128Mindestens ℕ₁₆ bzw. ℤ₁₆, Inferenztyp ℕ₃₂
−128Mindestens ℤ₈, Inferenztyp ℤ₃₂

Die Typen 𝕀 und 𝕌 stehen dem Programmierer nur eingeschränkt zur Verfügung. Im Gegensatz zu ℤ (und ℕ) handelt es sich bei 𝕀k und 𝕌k um ganze Zahlen, bei denen auch das Bit-Muster von Interesse ist. Dementsprechend sind diese als Zahlen im Zweierkomplement definiert, bei denen das niedrigstwertige Bit den Index 0 und das höchstwertige den Index k − 1 hat.

Im Gegensatz zu den k hat ein 𝕌k als vorzeichenlose Zahl ein anderes Überlaufverhalten und einen größeren Wertebereich. Damit können in einem 𝕌k Werte bis 2k − 1 (und nicht nur bis 2k − 1 − 1) gespeichert werden.

(…FFFF00)₁₆Mindestens 16 Bit, vorzeichenbehaftet.
(10 … 10)₂Mindestens 2 Bit, vorzeichenlos.
(FF 00 … 00 FF)₁₆Mindestens 24 Bit, vorzeichenlos.
(…0123 4567)₁₆Mindestens 32 Bit, vorzeichenbehaftet.

Im Zusammenhang mit den 𝕌k ist erwähnenswert, dass sie im Zusammenspiel mit den k und k immer das korrekte Ergebnis oder einen Überlauf liefern. Dabei wird allerdings das Zwischenergebnis in einem k abgelegt, dessen Zahlenraum dem des korrespondierendem k entspricht.

u : 𝕌₃₂ ← 2³¹Passt.
z : ₃₂ ← −2³¹Passt auch.
assertz throws Overflow 2³¹ ∉ ℤ₃₂.
assert z + u = 00 ∈ ℤ₃₂.
assert uz throws Overflow−2³¹ ∉ 𝕌₃₂.
assert zu throws Overflow2³¹ ∉ ℤ₃₂.
assert zz + uO. K.
assert uu + zO. K.
assert uu + z + 1 throws OverflowZwischenergebnis zu groß.
assert uuz ⊞ 1Passt auf jeden Fall, aber das Ergebnis…
Rationale Zahlen

Hierbei ist zu beachten, dass es sich bei den rationalen Zahlen um ein Paar der angegebenen Größe handelt. Ein ℚ₆₄ wird demnach in 128 Bit (zwei 64-Bit-Wörtern) abgelegt.

Gleitkommazahlen
299 792 45𝟖Exakt.
0.253̅4̅Periodisch.
3.1415926…Ungenau.
6.022 140 857 (74) × 10²³Mit Ungenauigkeit.
Dezimalzahlen

Bei den Dezimalzahlen gibt das Subskript an, welchen Speicherbedarf die Zahl hat. Bei Angabe eines einfachen Subskripts (typischerweise eine Zweierpotenz) handelt es sich um die Anzahl der verfügbaren Bits. Bei der Angabe von zwei, geklammerten und durch ein Komma getrennte Zahlen (n, m), gibt n die (mindestens) zur Verfügung stehenden Anzahl von Ziffern vor dem Komma der zu repräsentierten Zahl an und m die Nachkommastellen. Darüber hinaus kann das Subskript annotiert werden, was etwa die Ausgabe beeinflusst.

𝔻Beliebig genaue Dezimalzahl sm / 10e, s ∈ {−1, +1}, m, e.
𝔻₃₂Dezimale Gleitpunktzahl mit maximal 8 Dezimalstellen.
𝔻₆₄Dezimale Gleitpunktzahl mit maximal 17 Dezimalstellen.
𝔻₁₂₈Dezimale Gleitpunktzahl mit maximal 36 Dezimalstellen.
𝔻(5, ∗)Dezimale Gleitpunktzahl mit 5 Dezimalstellen.
𝔻(5, 2)Dezimale Fixpunktzahl mit 5 Dezimalstellen vor und 2 Dezimalstellen nach dem Komma.
d : 𝔻(5, ∗)← 2.52.5
d : 𝔻(5, 2)← 2.52.50
d : 𝔻(05, 2)← 2.500002.50
d : 𝔻(+5, 2)← 2.5+2.50
d : 𝔻(18, 5)← 2.52.50000
Zeichen

Um Zeichen zu speichern stehen die unterschiedlichen Typen zur Verfügung. Dabei erlaubt 𝔸₇ nur ASCII-Zeichen, 𝔸₈ nur Zeichen gemäß ISO/IEC 8859-1, 𝔸₁₆ nur Unicode-Zeichen aus der Basic Multilingual Plane (BMP) und schließlich 𝔸 alle im Unicode definierte Zeichen. Dabei schließen 𝔸₁₆ und 𝔸 die sogenannten Surrogate U+D800U+DFFF explizit aus. Für die Behandlung einzelnener Zeichen von UTF-16-kodierten Zeichenketten, die Surrogatpaare enthalten dürfen, gibt es den Typ Utf₁₆. Bei diesem ist allerdings darauf zu achten, dass die meisten Routinen zur Behandlung von Zeichenketten unter 􊨖lgola keine einzelnen Surrogate erlauben.

c₇ : 𝔸← ‘AU+0041 LATIN CAPITAL LETTER A{LATIN CAPITAL LETTER A}
assert c₇ = U+0041
c₈ : 𝔸← ‘ÄU+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS{LATIN CAPITAL LETTER A WITH DIARESIS}
assert c₈ = U+00C4
c₁₆ : 𝔸₁₆← ‘U+212B ANGSTROM SIGN{ANGSTROM SIGN}
assert c₁₆ = U+212B
c₂₁ : 𝔸 ← ‘𝔸U+1D538 MATHEMATICAL DOUBLE-STRUCK CAPITAL A{MATHEMATICAL DOUBLE-STRUCK CAPITAL A}
assert c₂₁ = U+1D538
Zeichenketten

Bemerkenswert an Zeichenketten in 􊨖lgola ist eigentlich nur, dass kaum Aussagen darüber gemacht werden können, wie sie gespeichert sind. Eine besondere UTF-16-Unterstützung ist zwar garantiert, aber die Zeichenkette selbst kann irgendwie gespeichert sein: als UTF-8, als Sequenz von Unicodes, komprimiert, verschlüsselt und was sonst noch das Herz begehrt. Nach außen hin gibt es also nur Unicode-Zeichen, womit das lästige Behandeln von Surrogatpaaren entfällt.

Der Umstand, dass alle Zeichenketten eine gemeinsame Schnittstelle implementieren, erlaubt auch das Arbeiten mit ausgefalleren Implementierungen, die etwa das Kopieren von Zeichenketten überflüssig machen können. Für den Fall, dass die Unveränderlichkeit garantiert werden muss, werden verschiedene Implementierung seitens 􊨖lgola angeboten, die auch verschlüsselte Varianten enthält.

Standarddatenstrukturen

LiteralDatentypBeschreibung
{1, 2, 3}Set⟦T⟧Menge
⦃1, 2, 2, 3⦄Bag⟦T⟧Beutel, Multimenge
{1 ↦ ‘a’, 2 ↦ ‘b’, 3 ↦ ‘c’}Map⟦K, V⟧Abbildung, Map
⟨1, 2, 3⟩List⟦T⟧(Verkettete) Liste
[1, 2, 3]Array⟦T⟧Reihung
[1 2 3]Vector⟦T, dim⟧Vektor
[1  2  3;  4  5  6]Matrix⟦T, dim₁, dim₂⟧Matrix
(1, 2, 3)nTuple⟦T₁, … , Tn⟧n-Tuple

Steuerungsstrukturen

Bei den Steuerungsstrukturen macht sich bemerkbar, dass unter 􊨖lgola möglichst genau beschrieben werden soll, was man zu erreichen sucht. Neben den üblichen Steuerungsstrukturen gibt es also Verallgemeinerungen und Spezialisierungen, mit deren Hilfe deutlich ausdrucksstärkere Programme geschrieben werden können.

Bedingte Anweisungen

Die Steuerungsanweisung schlechthin ist die bedingte Anweisung. Diese wird erwartungsgemäß mit dem Schlüsselwort if eingeleitet.

Sicherstellen, dass i der kleinere der beiden gefundenen Indizes ist, damit i₁ ≤ i gilt.
if i₁ < ithen
(i₁, i₂) ← (i₂, i₁)
end

Hier stellt sich eigentlich nur die Frage, wie mit den Alternativen umgegangen wird. Unter 􊨖lgola wird eine bedingte Alternative mit elseif eingeleitet, wodurch das verrufene schwebende else (engl.: dangling else) vermieden wird.

function numberOfDigits(x : ₁₆) → {1, … , 5} ⁌

Liefert die Anzahl der Ziffern einer nicht negativen, ganzen 16-Bit-Zahl.

is
if x ≤ 9 then
return 1
elseif x ≤ 99 then
return 2
elseif x ≤ 999 then
return 3
elseif x ≤ 9999 then
return 4
else
return 5
end
end

Die Bedingungen werden wie gewohnt von oben nach unten durchprobiert, der Block bei der ersten erfüllten Bedingung oder spätestens – wenn vorhanden – beim else ausgeführt und mit der ersten Anweisung hinter der gesamten if-Anweisung fortgefahren.

Alternativ zum if gibt es noch das unless, bei dem die Bedingung negiert werden muss und neben dem (auch optionalen) else keine weitere Verzweigung möglich ist. Das unless wird in der Regel aber nur im Zusammenhang mit dem bedingungslosen Sprung in der maschinennahen Programmierung benötigt, und kann deswegen nur mit entsprechenden Privilegien genutzt werden.

Mit Hilfe der case-of-Anweisung können zusammenhängende Fälle etwas besser dargestellt werden:

function numberOfDigits(x : ₁₆) → {1, … , 5} ⁌

Liefert die Anzahl der Ziffern einer nicht negativen, ganzen 16-Bit-Zahl.

is
case of
x ≤ 9:
return 1
x ≤ 99:
return 2
x ≤ 999:
return 3
x ≤ 9999:
return 4
◼ :
return 5
end
end

Wird die case-Anweisung an eine oder mehrere Variablen gebunden, so kann der Übersetzer eine unter bestimmten Umständen eine besser geeignete Reihenfolge festlegen.

case x of
◼ 0, …, 9:
return 1
◼ 10, …, 99:
return 2
◼ 100, …, 999:
return 3
◼ 1000, …, 9999:
return 4
◼ :
return 5
end

Bei für den Übersetzter erkennbaren überschneidungsfreien Bedingungen, kann bei geordneten Objekten die Anzahl der Tests bei n Tests in der Regel auf logn Tests beschränkt werden. Darüber hinaus besteht die Möglichkeit die häufigsten Fälle zu markieren, so dass der Übersetzter dies bei einer eventuellen Umverteilung der Bedingungen berücksichtigen kann.

Auch bei nicht zusammenhängenden Bereichen oder ungeordneten Objekten kann der Übersetzer geeignete Strategien nutzen, die den Aufwand, eine Übereinstimmung mit den aufgezählten Objekten zu finden, minimieren.

case s of
◼ “abc”:
◼ “def”:
◼ “ghi”:
end

Bei dieser case-Anweisung kann beispielsweise Code generiert werden, der – wenn sicher gestellt wurde, dass die gegebene Zeichenkette s nicht leer ist – die letzten zwei Bit des ersten Zeichens von s dazu nutzt, in einer Tabelle die passende Sprungadresse zu ermitteln und so mit nur einem Vergleich von Zeichenketten entscheiden zu können, ob s einem der Fälle entspricht.

Schleifen

􊨖lgola bietet neben seinem sehr allgemeinen Schleifenkonstrukt viele spezialisierte Varianten davon, die sich auch in anderen Programiersprachen wiederfinden. Ungewöhnlich ist dabei vielleicht, dass sich alle Steuerungsanweisungen im Schleifenkopf befinden können, was insbesondere bei der nicht abweisenden Schleife zu anfänglichen Irritationen führen kann.

Das allgemeine Schleifenkonstrukt wird durch das Schlüsselwort loop eingeleitet und der dazugehörige Schleifenkopf mit do beendet, womit auch der auszuführende Schleifenrumpf eingeleitet wird. Die einfachste Schleife ist dann die Endlosschleife.

loop do
r :← random(1, … , 100)
if r = 55 then
breakMuss bei „Endlosschleifen“ vorhanden sein, wenn sie nicht mit endless annotiert wurde.
end
end

Der Schleifenkopf kann dann mit den folgenden Anweisungen (hier in logischer Reihenfolge gegeben) verfeinert werden:

Alle diese Schleifenteile sind optional, müssen aber, wenn sie angegeben werden, innerhalb des Kopfes immer in der folgenden Reihenfolge geschrieben werden: init, doing, while, inbetween, before, after, until, finally. Das inbetween darf aber näher an den Rumpf gesetzt werden, wenn die darin stehenden Anweisungen eher den Rumpfanweisungen zugeordnet werden können.

Will man etwa mit Hilfe eines Iterators explizit über die Elemente einer Objektsammlung iterieren, so kann man dies unter Java mit einer while-Schleife ermöglichen.

it :← C.iterator()
while it.hasNext() do
element :← it.next()
doSomethingWith(element)
end

Das hat allerdings den Nachteil, dass der Iterator, auch über die Lebensdauer der Schleife hinaus, bekannt bleibt. Ähnlich ist es mit dem Element, das zwar klar ein Teil der Iteration ist, hier aber wie ein Teil des Schleifenrumpfs sichtbar wird. Die 􊨖lgola-Lösung erlaubt nun eine strikte Trennung, mit der alle Teile der Iteration im Schleifenkopf untergebracht werden können.

loop
initit :← C.iterator()
whileit.hasNext()
beforeelement :← it.next()
do
doSomethingWith(element)
end

Mit Hilfe des allgemeinen Schleife lässt sich unter 􊨖lgola auch das Problem der n + ½ Schleifendurchläufe lösen. Will man etwa auf eine Datei zugreifen und liefert ein read neben den zu lesenden Bytes auch einen besonderen Wert, z. B. −1, dann lässt sich das in C-artigen Sprachen nur etwas schwerfällig wie folgt lösen:

char c;
while ((c = in.read()) != −1) {
}

Ungeachtet des bereits oben geschilderten Problems der Sichtbarkeit von c wäre diese Implementierung in 􊨖lgola gar nicht möglich, da Zuweisungen keinen Rückgabewert haben (können). Das Problem kann aber ohnehin viel besser mit Hilfe des doing gelöst werden.

loop
doingc :← read()
whilec ≠ −1
do
end

Da bei diesen Schleifen das loop-Konstrukt ein bisschen sperrig wirkt, kann man die häufig wiederkehrenden Muster auch kürzer schreiben bzw. mit dem wesentlichen Schlüsselwort einleiten.

Wird wahrscheinlich einer while-doing-Variante zum Opfer fallen.
doing c :← read() while c ≠ −1 do
end
while i < n step ii + 1 do
end
until i < n step ii + 1 do
end

Dabei ist wie gewohnt die while-Schleife eine abweisende Schleife, die nur bei erfüllter Bedingung durchlaufen wird, derweil die until-Schleife als nicht abweisende Schleife mindestens ein Mal ausgeführt wird.

Ein immer wiederkehrendes Problem ist das Ausführen von Anweisungen zwischen je zwei Schleifendurchläufen. Soll etwa eine Liste Komma-separiert ausgegeben werden, so darf das Komma natürlich nur zwischen den Elementen stehen. Dies kann im Normalfall nur durch einen zusätzlichen Test – eventuell verbunden mit dem Einführen einer Boole’schen Variablen – erledigt werden.

loop
initflag :← false
do
if flag then
print(“, ”)
else
flagtrue
end
print(element)
end

Auch wenn mit Hilfe des loop-Konstrukts die Lesbarkeit schon deutlich verbessert werden kann, ist das Ergebnis dennoch suboptimal.

loop
initis_inbetween :← false
after is_inbetweentrue
do
if is_inbetween then
print(“, ”)
end
print(element)
end

Deshalb verdient das inbetween besonderes Augenmerk, da mit diesem trivial Anweisungen zwischen die Rumpfdurchläufe eingefürgt werden können. Eine Liste Komma-separiert auszugeben kann dann wie folgt bewerkstelligt werden:

loop
inbetween print(“, ”)
do
print(s)
end
printLine()

Das inbetween kann natürlich in allen Schleifen eingesetzt werden.

Die for-Schleife

Während das loop-Konstrukt beliebige Bedingungen zulässt und darüber hinaus die Schleifen mit break und continue vorzeitig verlassen bzw. fortgesetzt werden können, will man doch oft nur über bestimmte Objekte iterieren: über alle Elemente einer Objektsammlung, die geordnete Indexmenge eines Arrays oder alle Paare einer Abbildung. Mit der for-Schleife lassen sich diese Wünsche besser erfüllen, so dass man sich nicht um das Iterieren kümmern und dabei trotzdem nicht auf die explizite Schleifenkontrolle mit break und continue verzichten muss.

for xC do
(x)
end

Hier wird der Block für jedes Element aus der Objektsammlung C genau einmal durchlaufen und das Element kann über die angegebene Variable – hier x – angesprochen werden. Die Variable selbst kann innerhalb der Schleife nicht verändert werden (der Inhalt des referenzierten Objekts möglicherweise wohl). Ist diese Laufvariable bereits bekannt, so enthält sie die nach Ablauf der Schleife das zuletzt bearbeitete Element.

x :←
for xC do
if p(x) then
break
end
end
printLine(x)

Hier wird also dasjenige x ausgegeben, das die Eigenschaft p hat, oder – im Fall, dass C keine Elemente hat – den Wert .

Beim Durchlaufen der Elemente werden die Eigenschaften der Sammlung berücksichtigt. Eine Menge etwa durchläuft seine Elemente in beliebiger Reihenfolge; dabei kann noch nicht einmal gewährleistet werden, dass bei zweimaligem Laufen die selbe Reihenfolge beibehalten wird. Dahingegen wird bei einer Liste immer deren Ordnung widergespiegelt, so dass bei Durchlaufen derselben Liste die Elemente immer in der exakt selben Reihenfolge durchlaufen werden.

C := {1, 2, 3}
sb := SetBuilder()
for xC do
sb.add(2𝑥)
end
S := sb.done()
assert S = {2, 4, 6}
C := {1, 2, 3}
S := {2𝑥xC}
assert S = {2, 4, 6}

Abbilden, filtern und reduzieren

Filtern

{pppersons, p.salaryC}

Abbilden

{p.nameppersons}

Reduzieren

max({p.nameppersons, p.salaryC})
∑{p.salaryppersons, p.salaryC}Ungeordnet.
∑⟨p.salaryppersons, p.salaryCGeordnet.
if ∃(ppersons) (p.salaryC) then
printLine(p.salary)
end
if ∀(ppersons) (p.salaryC) then
end
inline
function signumk : ⟧(x : k⟧) → Zk⟧{−1, 0, +1} is
variant
isNative(shiftRight):
return (x ≫ (k − 1)) ⦶ (x ⋙ (k − 1))
◼ ¬ isNative(shiftRight):
return −(x ⋙ (k − 1)) ⦶ (x ⋙ (k − 1))
◼ :
return [x > 0] − [x < 0]
end
end

Erweiterung des Gültigkeitsbereichs (Skopus)

Eine der wichtigsten Prinzipien der Programmierung unter 􊨖lgola ist das Lokalitätsprinzip. Möglichst viel von dem, was benötigt wird, soll so nah wie möglich beieinander stehen. Das gilt etwa für gemeinsame Ausdrücke wie k − 1 in nachfolgender Berechnung.

s := k − 1
return (xs) ⦶ (xs)

Damit solche Berechnungen nicht den jeweiligen Gültigkeitsbereich verwässern, gibt es die Möglichkeit Variablen für gemeinsame Teillausdrücke zu formulieren, die dann vom voranstehenden Ausdruck verwendet werden können.

return (xs) ⦶ (xs) where s := k − 1

Mit Ende der Anweisung verlieren dementsprechend auch die Variablen ihre Gültigkeit.

Objekte

Alle Daten in 􊨖lgola, ob veränderlich oder nicht, sind Objekte. Deren Attribute und Methoden werden in einer object-Anweisung formuliert.

object Color

Beschreibt ine Farbe im RGB-Farbraum.

is
red: 𝕌
green: 𝕌
blue: 𝕌
init (r : ₃₂{0, … , 255}, g : ₃₂{0, … , 255}, b : ₃₂{0, … , 255}) is
redr
greeng
blueb
end
method lighter() → Color

Ungeschickte Implementierung, um diese Farbe heller zu machen.

is
return Color(red ∔ 10, green ∔ 10, blue ∔ 10)Saturierte Addition
end
method toHtmlColor() → 𝕊

Liefert eine hexadezimale HTML-Repräsentation dieser Farbe.

is
s :← StringBuilder(7)
toHtmlColor(s)
return s.toString()
end
method toHtmlColor(Appendable appendable) → () ⁌

Ergänzt eine hexadezimale HTML-Repräsentation dieser Farbe.

is
“#”.toString(appendable)
red.toHexString(appendable, 2)
green.toHexString(appendable, 2)
blue.toHexString(appendable, 2)
end
end

Bezüglich der Ordnung von Attributen und Methoden gibt es zwar keine Vorschriften, aber typischerweise stehen die Attribute vor den jeweiligen Methoden.

Man kann sich dies nun so vorstellen, dass diese object-Anweisung ein Meta-Objekt – die Klasse – manipuliert, die – über die Lebensdauer des Übersetzers hinaus – als Klassendatei persistiert wird. Dieses Meta-Objekt oder diese Klasse wird zur Laufzeit bei Bedarf geladen und steht über den Namen zur Verfügung, der bei der object-Anweisung angegeben wurde. Über das so benannte Meta-Objekt können nun Objekte dieser Art erzeugt werden. Jenes sorgt also für den Speicher, garantiert dessen Initalisierung und kann darüber hinaus noch beliebige andere Anweisungen ausführen, die für das Meta-Objekt spezifisch sind.

red:← Color(255, 0, 0)
medium_turquoise:← Color((48)₁₆, (D1)₁₆, (CC)₁₆)
wrong:← Color(500, 0, 0)↯ 500 ∉ {0, … , 255}

Obwohl das Meta-Objekt selbst keine Routine ist, wird hier eine Methode aufgerufen. Allgemein wird für jedes Objekt o, das keine Routine ist, bei dem Ausdruck o‌(p₁, … , pn) mit n ≥ 0 die zu den übergebenen Parametern pi passende apply-Methode, also o.apply(p₁, … , pn) aufgerufen.

In den meisten Fällen ist die Kenntnis über dieses Vorgehen nicht erforderlich und könnte auch einfach damit erklärt werden, dass ein Objekt der Klasse O eben über O(p₁, … , pn) erzeugt wird. Allerdings ist es dann schwerer zu verstehen, dass in Abhängigkeit vom Meta-Objekt eventuell immer nur das selbe Exemplar geliefert wird (Singleton), das zu den Parametern passende, eindeutige Exemplar (Flyweight) oder gar ein Objekt einer völlig anderen Klasse (Factory), das dann natürlich dem Liskov’schen Substitutionsprinzip genügt.

object Employee is
immutable
name : 𝕊
salary : 𝔻₆₄
init (name : 𝕊) is
self.name name
self.salary ← 0
end
setter
method salary(salary : 𝔻₆₄) → ()
requires {salary > 0}
is
self.salarysalary
end
getter
method salary() → 𝔻₆₄ is
return salary
end
override
method toString(Appendable appendable) → () is
name.toString(appendable)
“: ”.toString(appendable)
salary.toString(appendable)
end
end object

Alle Attribute müssen vor ihrer Nutzung initialisiert werden. Dafür ist die init-Methode zuständig; eine automatische Initialisierung – wie etwa in Java, bei der alle Attribute mit Null-Werten belegt werden – gibt es nicht. Es kann mehrere init-Methoden geben, aber es gibt in der Regel eine primäre Methode, die auch entsprechend ausgezeichnet werden kann.

Ein Objekt o : O wird über sein Meta-Objekt Klasse O erzeugt. Für jede init-Methode wird nämlich in dem zum Objekt gehören

o₁ : O
o₁ ← O()
o₂ :← O()

Zugriffsrechte

Über Annotationen kann geregelt werden, wie andere Objekte auf Attribute und Methoden zugreifen können.

Während die meisten dieser Zugriffsrechte auch aus anderen Sprachen bekannt sein dürften, verdient nur das privileged besondere Beachtung. Damit ist es beispielsweise möglich, sogar dem Objekt selbst den Zugriff auf eigene Attribute zu verbieten.

object Component is
privileged(owner: Container)⟧
p : Point
end
object Container isa Component is
children : MutableListComponent
method add(Component c) → () is
c.pPoint(…)
children.add(c)
end
end

Schnittstellen

interface Color is
immutable
field red: ₃₂{0, … , 255}
immutable
field green: ₃₂{0, … , 255}
immutable
field blue: ₃₂{0, … , 255}
convenience
method toHtmlColor() → 𝕊

Liefert eine hexadezimale HTML-Repräsentation dieser Farbe.

is
s :← StringBuilder(7)
toHtmlColor(s)
return s.toString()
end
method toHtmlColor(Appendable appendable) → () ⁌

Ergänzt eine hexadezimale HTML-Repräsentation dieser Farbe.

is
“#”.toString(appendable)
red.toHexString(appendable, 2)
green.toHexString(appendable, 2)
blue.toHexString(appendable, 2)
end
end
c : Color ← …
c.redLiefert den Rot-Anteil der Farbe unanhägig von der konkreten Implementierung.
object RgbColor isa Color

3-Byte-Version einer RGB-Farbe.

is
r : 𝕌
g : 𝕌
b : 𝕌
init (r : 𝕌₈, g : 𝕌₈, b : 𝕌₈) is
self.rr
self.gg
self.bb
end
getter
method red() → ₃₂{0, … , 255} is
return r
end
getter
method green() → ₃₂{0, … , 255} is
return g
end
getter
method blue() → ₃₂{0, … , 255} is
return b
end
object RgbColor isa Color

Tetra-Version einer RGB-Farbe.

is
private
immutable
rgb : 𝕌₃₂
public
init (r : 𝕌₈, g : 𝕌₈, b : 𝕌₈) is
rgb ← (((r ⋉ 8) ⦶ g) ⋉ 8) ⦶ b
end
getter
method red() → ₃₂{0, … , 255} is
return rgb ⋊ 16
end
getter
method green() → ₃₂{0, … , 255} is
return (rgb ⋊ 8) ⊙ (FF)₁₆
end
getter
method blue() → ₃₂{0, … , 255} is
return rgb ⊙ (FF)₁₆
end
object RgbColor isa Color

Tetra-Version einer RGB-Farbe.

is
private
const B := 256
private
immutable
rgb : 𝕌₃₂
public
init (r : ₃₂{0, … , 255}, g : ₃₂{0, … , 255}, b : ₃₂{0, … , 255}) is
rgb ← (((rB) + g) ⋅ B + b
end
getter
method red() → ₃₂{0, … , 255} is
return ediv(rgb, B²)
end
getter
method green() → ₃₂{0, … , 255} is
return emod(ediv(rgb, B), B)
end
getter
method blue() → ₃₂{0, … , 255} is
return emod(rgb, B)
end
object HsvColor extends RgbColor is
typealias Degree 𝔽₃₂{x ∣ x ∈ [0 ; 360.0[}[°]
typealias Percent 𝔽₃₂{x ∣ x ∈ [0 ; 1]}[%]
immutable
hue : Degree
immutable
saturation : Percent
immutable
value : Percent
init (hue : Degree, saturation : Percent, value : Percent) is
(r, g, b) :← hsvToRgb(hue, saturation, value)
super.init(r, g, b)
self.huehue
self.saturationsaturation
self.valuevalue
end
function hsvToRgb(h : Degree, s : Percent, v : Percent) → (r : 𝕌₈, g : 𝕌₈, b : 𝕌₈) is
h′ :← h / 60°
hi : ₃₂ ← ⌊h′⌋
f :← h″ − h
p := v ⋅ (1 − s)
q := v ⋅ (1 − s‌f)
t := v ⋅ (1 − s‌(1 − f))
function convert(r : Percent, g : Percent, b : Percent) → (r : 𝕌₈, g : 𝕌₈, b : 𝕌₈) is
return (cvt‌(r), cvt‌(g), cvt‌(b)) where cvt : (x : Percent) ↦ 𝕌₈(⌊x ⋅ 256.⌋)
end
case hi of
◼ 0:
return convert(v, t, p)
◼ 1:
return convert(q, v, p)
◼ 2:
return convert(p, v, t)
◼ 3:
return convert(p, q, v)
◼ 4:
return convert(t, p, v)
◼ 5:
return convert(v, p, q)
end
end
end
interface Debugable is
public
method toDebugString(Appendable appendable) → ()
end
object Point isa Debugable is
x : ₃₂
y : ₃₂
override
method toDebugString(Appendable appendable) → () is
“Point(”.toString(appendable)
x.toString(appendable)
“, ”.toString(appendable)
y.toString(appendable)
“)”.toString(appendable)
end
end
interface Debugable isa 𝕆 is
private
method appendNameAndId(Appendable appendable) → () is
getClass().toString(appendable)
“@”.toString(appendable)
identityHashCode(self).toString(appendable)
end
public
method toDebugString(Appendable appendable) → () is
appendNameAndId((appendable)
self.toString(appendable)
end
end

Vererbung

Während die Vererbung bei Schnittstellen inhärent ist, muss sie bei Objekten explizit möglich gemacht werden.

open
object Applet is
end
object Program extends Applet is
end

Hierbei ist besonders hervorzuheben, dass auch die Initialisierer wie „normale“ Methoden behandelt werden. Das bedeutet insbesondere, dass auch Initialisierer vererbt und abstrakt sein können.

Routinen

Zunächst lassen sich alle Routinen in zwei große Gruppen einteilen: statisch gebundene und dynamisch gebundene. Statisch gebundene Routinen werden zur Übersetzungszeit ausgewählt und sind dadurch über die komplette Laufzeit eines Programms festgelegt. Die dynamischen Routinen hingegen sind an den Laufzeit-Typ meist eines der Parameter gebunden. In diesem Fall wird über das konkrete Objekt entschieden, welche der möglichen Routinen ausgeführt wird. Zu der letzten Art gehören insbesondere die Methoden.

Unabhängig davon lassen sich die Routinen wieder in zwei Gruppen zerlegen: in die Routinen mit und in die ohne Nebeneffekte. Um dies zu dokumentieren gibt es neben den Methoden, über die in der Regel keine allgemeine Aussagen gemacht werden, die Funktionen und Prozeduren. Funktionen haben niemals Nebeneffekte und müssen deshalb mindestens einen Wert zurückgeben.

function sumOfDigits(x : ) →

Liefert die Quersumme – die Summe der Ziffern – der gegebenen Zahl.

is
sum :← 0
while x ≠ 0 do
d :← x mod 10
xx ÷ 10
sumsum + d
end
return sum
end

Eine Funktion hängt also nur von seinen Eingabeparametern ab (und darf diese auch definitionsbedingt nicht ändern). Eine Funktion, die keine Eingabeparameter hat, ist äquivalent zu einer Konstanten.

Eine Prozedur hat bezüglich der Nebeneffekte keine Einschränkungen. Damit ist es auch möglich Prozeduren zu definieren, die keinen Rückgabewert haben.

procedure initialiseArray(a : MutableArray₃₂⟧) → () ⁌

Initialisiert das gegebene Array derart, dass für alle Elemente

a[i] = i

gilt.

is
for i :↞ 1, … , ∣ado
a[i] ← i
end
end

Übergabemechanismen

Grundsätzlich werden alle Argumente einer Routine strikt – also vor ihrer Übergabe – von links nach rechts ausgewertet und der resultierende Wert übergeben.

Wertübergabe – call by value

Wenn nichts anderes explizit angegeben wird, findet die Übergabe über den Wert eines Ausdrucks statt. Ausdrücke werden zuerst vollständig ausgewertet und deren Wert wird an die aufgerufene Routine übergeben.

x :← 1
y :← 2
f(x + y)f(3)

Das bedeutet für Variablen, die Objekte beschreiben, dass die Referenz auf dieses Objekt übertragen wird. Handelt es sich um ein veränderliches Objekt, so kann die aufgerufene Funktion zwar den Inhalt des Objekts ändern, nach außen hin aber nicht die Referenz auf das Objekt.

procedure modify(m : Mutable) → () is
m.mutabor()Objektinhalt wird geändert.
mMutable()
end
test “Objektreferenz wird durch aufgerufene Routine nicht verändert” is
m :← Mutable()
old_reference :≡ mReferenzwert speichern
modify(m)
assert mold_referenceReferenzwert („Adresse“) ist unverändert.
end

Referenzübergabe – call by reference

test “Variable muss bei Variablenübergabe initialisiert sein” is
n :
compiler_fail
refVar(⟦refn)↯ n muss initialisiert sein!
end

Namensübergabe – call by name

Während die Referenzübergabe eigentlich nur ein technisches Detail beschreibt, definiert die Namensübergabe einen Aufruf über eine Referenz, die alle Eigenschaften der übergebenen (lokalen) Variable übernimmt. Das bedeutet insbesondere bei einer Änderung des Parameters, dass sich die so übergebene Variable ändert.

procedure initVar(⟦var(access: WRITE)⟧ v : ) → () ⁌

Die Annotation garantiert, dass die Variable vor ihrer Benutzung bzw. spätestens vor Verlassen der Routine initialisiert wird.

is
v ← 5
end
procedure refVar(⟦refv : ) → () is
v ← 6
end
test “Initialisierung via Variablenübergabe” is
n :
initVar(⟦varn)
assert n = 5
refVar(⟦refn)
assert n = 6
end

Verzögerte Übergabe – call by need

Gelegentlich ist es vonnöten, dass nicht alle Ausdrücke vor der Übergabe ausgewertet werden, sondern erst dann, wenn dies nötig ist. Bekannteste Beispiele dafür sind die Kurzschlussoperatoren für das logische Oder und das logische Und, bei denen das zweite Argument nur dann ausgewertet wird, wenn es das Resultat des ersten dies erforderlich macht.

function . ⩑ . : (a : 𝔹, ⟦lazy(ALL)⟧ b : 𝔹) → 𝔹 is
if ¬a then
return false
else
return b
end
end
if pnilp.x ≥ 0 then
Würde die zweite Bedingung immer ausgewertet werden, so käme es zu einem Zugriffsfehler, wenn die erste Bedingung nicht erfüllt ist.
end

Mit dem Zusatz all in der lazy-Annotation ist es auf der Seite des Aufrufers nicht erforderlich, dass die lazy-Annotation ebenfalls angegeben werden muss. Im Standardfall soll damit nämlich sichergestellt werden, dass auf der Aufruferseite speziell bei Ausdrücken mit Nebeneffekten erkannt wird, dass der betreffende Ausdruck eventuell nicht oder sogar mehrfach ausgeführt wird.

procedure multiply(n : , ⟦lazyx : ) → is
total :← 0
times n do
totaltotal + xx wird höchstens ein einziges Mal ausgewertet.
end
x wurde bei n = 0 kein Mal ausgewertet
return total
end

Vorgegebene Werte für Parameter

Auch wenn 􊨖lgola das Überladen von Funktionen erlaubt, kann es dennoch nützlich sein, dieses zu minimieren, indem Parametern Grundwerte zugeordnet werden.

procedure createList(from : ₃₂, to : ₃₂, δ : ₃₂ = 1) → List₃₂⟧
requires {fromto, δ > 0}

Erzeugt eine Liste, die man besser mit from, from + δ, … , to erzeugt hätte.

is
list :← ListBuilder₃₂⟧()
loop
initi :← from
whileito
stepii + δ
do
list.add(i)
end
return list.done()
end
test “Wertvorgabe ist 1” is
assert createList(1, 10) = ⟨1, 2, 3, 4, 5, 6, 7, 8, 9, 10⟩
assert createList(1, 10) = createList(1, 10, 1)
end

Bei der Vorbesetzung von Parametern kann dann auch auf bereits definierte Parameter zurückgegriffen werden:

procedure convertToString(a : Array𝔸⟧, offset : ₃₂ = 0, length : ₃₂ = ∣a∣ − offset) → 𝕊
requires {offsetoffset + length < ∣a∣}

Liefert die gewünschte Anzahl Zeichen aus dem Array ab der gegebenen Stelle und liefert diese als Zeichenkette.

is
end
test is
s := [‘h’, ‘a’, ‘l’, ‘l’, ‘i’, ‘h’, ‘a’, ‘l’, ‘l’, ‘o’]
assert convertToString(s) = “hallihallo”
assert convertToString(s, 5) = “hallo”
assert convertToString(s, 5, 3) = “hal”
end

Benannte Parameter

function makeTuple(
fixed : ,
fixed_optional : = 10,
“named_optional” : = 20,
“named_required” : ,
renamed_optional “x” : = 30
) → is
return (fixed, fixed_optional, named_optional, named_required, renamed_optional)
end
assert makeTuple(5, x: 22, named_required: 11)= (5, 10, 20, 11, 22)
assert makeTuple(5, named_required: 11, named_optional: 17)= (5, 10, 17, 11, 30)
assert makeTuple(5, named_required: 11)= (5, 10, 20, 11, 30)
function move(p : Point, “Δx” : ₃₂ = 0, δy “Δy” : ₃₂ = 0) → Point is
return Point(p.x + Δx, p.y + δy)
end
p :← Point(0, 0)
assert move(p, Δx: −2, Δy: 2) = Point(−2, 2)
assert move(p, Δy: 2, Δx: −2) = Point(−2, 2)
assert move(p, Δx: 2) = Point(2, 0)
assert move(p, Δy: 2) = Point(0, 2)
assert move(p) = p
assert move(p, −2, 2) = Point(−2, 2)↯ Explizite Argumentenmarken erforderlich.

Variable Argumentenliste

Unter 􊨖lgola ist es auch möglich eine variable Argumentenliste anzugeben. Wird der Name des letzten Parameters einer Routine mit (hochgestelltem) Asterisk * oder Plus + annotiert, so können an dieser Stelle beliebig viele Argumente des angegebenen Typs aufgelistet werden. Bei Verwendung des Pluszeichens muss an dieser Stelle mindestens ein Argument angegeben werden.

function makeList(elements : ) → Listis
list :← ListBuilder(∣elements∣)
for eelements do
list.add(e)
end
return list.done()
end
assert makeList(1, 2, 3, 4, 5) = ⟨1, 2, 3, 4, 5⟩
function ⟪.⟫ : (elements : ) → Listis
return makeList(∗elements)
end
assert ⟪1, 2, 3, 4, 5⟫ = ⟨1, 2, 3, 4, 5⟩
function switch(i : , ⟦lazyexpressions+ : ) →
requires {1 ≤ n ≤ ∣expressions∣}
is
return expressions[i]
end
assert switch(2, ack(3, 10), 10, ack(3, 20)) = 10
function ack(n : , m : ) → is
◼ (0, m):
return m + 1
◼ (n, 0):
return ack(n − 1, 1)
◼ (n, m):
return ack(n − 1, ack(n, m − 1))
end

Tests

object O is
function f() → is
return 1
end
function g() → is
return h()↯ Kann nur in Test-Funktionen verwendet werden.
end
test
method h() → is
return f()
end
test
method i() → is
return f() + h()
end
test “Test” is
oO()
assert o.f() = 1
assert o.h() + o.i() = 3Dürfen natürlich in einem Test verwendet werden.
end
end
test “optionaler Text zur Beschreibung/Identifzierung eines Tests” is
assert div(2, 2) = 1
assert div(2, 1) = 2
assert div(2, 0) throws ArithmeticException
end
test divx : ₃₂, y : ₃₂⦆ → ₃₂ is
◻ (2, 2)↦ 1
◻ (2, 1)↦ 2
◻ (2, 0)throws ArithmeticException
end