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:
⟦override⟧ |
method run() → () is |
printLine(“Hello World!”) |
Das ist allerdings suboptimal, denn diese Applikation – so klein sie auch sein mag – ist nicht internationalisierbar. Korrekt müsste es also eher
message HELLO_WORLD en “Hello World!” |
⟦override⟧ |
method run() → () is |
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.
message HELLO_WORLD en “Hello \{1:𝕊}!” |
printLine(HELLO_WORLD, name) |
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:
- Kommando: Eine Schlüsselwort leitet eine einzeilige Anweisung ein. Dazu zählen etwa das namespace- oder import-Kommando, mit dem der Namensraum für extern verfügbare Elemente bestimmt wird bzw. die zur Übersetzung nötigen Hilfsmittel festgelegt werden; aber auch das break-Kommando zählt dazu, mit dem eine Schleife vorzeitig verlassen werden kann.
- Block-Anweisung: Ein Schlüsselwort leitet einen Block, der mit dem Schlüsselwort end beendet wird. Soll zusätzlich ein Kopfelement definiert werden, so wird dieses durch das einleitende Schlüsselwort und ein geeigneten Terminator (bei dem es sich eventuell nur um ein kontextbezogenes Schlüsselwort handelt) eingerahmt. Genauso kann ein Block durch Schlüsselwörter unterbrochen werden.
- Ausdrücke: Bei den Ausdrücken handelt es sich um alles, was nicht durch ein Schlüsselwort eingeleitet wird. Dazu zählen Ausdrücke etwa wie a + b. Aber auch die Zuweisung a ← b ist zunächst nur ein Ausdruck. Wegen der besonderen Bedeutung des Pfeil-Operators wird dieser Ausdruck aber als Anweisung interpretiert.
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 :← 1000 | Eine ganze Zahl |
p :← Person() | Eine Person |
Dabei erlaubt der Pfeil eine erneute Zuweisung, derweil ein Gleichheitszeichen eine Unveränderlichkeit der Variablen anzeigt.
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 :← 1 | Vermutlich ℕ₃₂ (ü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: ℕk ≡ ℤk{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 x ⊞ y = −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
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.
128 | Mindestens ℕ₁₆ bzw. ℤ₁₆, Inferenztyp ℕ₃₂ |
−128 | Mindestens ℤ₈, 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.
(…FF … FF00)₁₆ | 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. |
assert −z throws Overflow | 2³¹ ∉ ℤ₃₂. |
assert z + u = 0 | 0 ∈ ℤ₃₂. |
assert u ← z throws Overflow | −2³¹ ∉ 𝕌₃₂. |
assert z ← u throws Overflow | 2³¹ ∉ ℤ₃₂. |
assert z ← z + u | O. K. |
assert u ← u + z | O. K. |
assert u ← u + z + 1 throws Overflow | Zwischenergebnis zu groß. |
assert u ← u ⊞ z ⊞ 1 | Passt 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 s ⋅ m / 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.5 | 2.5 |
d : 𝔻(5, 2) | ← 2.5 | 2.50 |
d : 𝔻(05, 2) | ← 2.5 | 00002.50 |
d : 𝔻(+5, 2) | ← 2.5 | +2.50 |
d : 𝔻(18, 5) | ← 2.5 | 2.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+D800 – U+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₇ : 𝔸₇ | ← ‘A’ | U+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
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₁ < i₂ then |
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} ⁌
⁍ is |
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} ⁌
⁍ is |
Wird die case-Anweisung an eine oder mehrere Variablen gebunden, so kann der Übersetzer eine unter bestimmten Umständen eine besser geeignete Reihenfolge festlegen.
Bei für den Übersetzter erkennbaren überschneidungsfreien Bedingungen, kann bei geordneten Objekten die Anzahl der Tests bei n Tests in der Regel auf log₂ n 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.
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.
r :← random(1, … , 100) |
if r = 55 then |
break | Muss bei „Endlosschleifen“ vorhanden sein, wenn sie nicht mit endless annotiert wurde. |
Der Schleifenkopf kann dann mit den folgenden Anweisungen (hier in logischer Reihenfolge gegeben) verfeinert werden:
- init 𝒞: Wird ausgeführt, bevor irgendetwas anderes von der Schleife ausgeführt wird. Damit wird aber auch auf jeden Fall nach Beendigung der Schleife das finally – wenn vorhanden – durchlaufen.
- doing 𝒞: Wird unmittelbar vor jedem while-Test ausgeführt.
- while ℰ: Wird vor Ausführung des Schleifenrumpfs geprüft und führt diesen nur dann aus, wenn die gegebene Bedingung ℰ erfüllt ist.
- inbetween 𝒞: Wird vor dem Schleifenrumpf ausgeführt, aber nur, wenn der Schleifenrumpf schon einmal durchlaufen wurde.
- before 𝒞: Wird als Teil des Schleifenkopfs unmittelbar vor dem Block (also nach dem inbetween) ausgeführt.
- step 𝒞: Wird unmittelbar nach dem Schleifenrumpf noch vor dem until-Test ausgeführt.
- until ℰ: Wird erst nach der Ausführung der step-Anweisung geprüft und beendet die Schleife genau dann, wenn die gegebene Bedingung ℰ erfüllt ist.
- after 𝒞: Wird nach dem until ausgeführt, wenn die Schleife nicht zuvor beendet wurde.
- finally 𝒞: Wird auf jeden Fall vor Beendigung der Schleife ausgeführt. Dabei spielt es keine Rolle, ob die Schleife normal oder abrupt (also durch ein break, return oder eine Ausnahme) beendet wird.
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) |
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.
init | it :← C.iterator() |
while | it.hasNext() |
before | element :← it.next() |
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.
doing | c :← read() |
while | c ≠ −1 |
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 |
while i < n step i ← i + 1 do |
until i < n step i ← i + 1 do |
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.
Auch wenn mit Hilfe des loop-Konstrukts die Lesbarkeit schon deutlich verbessert werden kann, ist das Ergebnis dennoch suboptimal.
init | is_inbetween :← false |
… |
after is_inbetween ← true |
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:
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.
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.
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 x ∈ C do |
end |
S := sb.done() |
assert S = {2, 4, 6} |
C := {1, 2, 3} |
S := {2𝑥 ∣ x ∈ C} |
assert S = {2, 4, 6} |
Abbilden, filtern und reduzieren
Filtern
{p ∣ p ∈ persons, p.salary ≥ C} |
Abbilden
Reduzieren
max({p.name ∣ p ∈ persons, p.salary ≥ C}) |
∑{p.salary ∣ p ∈ persons, p.salary ≥ C} | Ungeordnet. |
∑⟨p.salary ∣ p ∈ persons, p.salary ≥ C⟩ | Geordnet. |
if ∃(p ∈ persons) (p.salary ≥ C) then |
if ∀(p ∈ persons) (p.salary ≥ C) then |
⟦inline⟧ |
function signum⟦k : ℕ⟧(x : ℤ⟦k⟧) → Z⟦k⟧{−1, 0, +1} is |
variant |
◼ isNative(shiftRight): |
return (x ≫ (k − 1)) ⦶ (x ⋙ (k − 1)) |
◼ ¬ isNative(shiftRight): |
return −(x ⋙ (k − 1)) ⦶ (x ⋙ (k − 1)) |
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 (x ≫ s) ⦶ (x ⋙ s) |
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 (x ≫ s) ⦶ (x ⋙ s) 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.
red | : 𝕌₈ |
green | : 𝕌₈ |
blue | : 𝕌₈ |
init (r : ℕ₃₂{0, … , 255}, g : ℕ₃₂{0, … , 255}, b : ℕ₃₂{0, … , 255}) is |
end |
method lighter() → Color ⁌
⁍ is |
return Color(red ∔ 10, green ∔ 10, blue ∔ 10) | Saturierte Addition |
end |
method toHtmlColor() → 𝕊 ⁌
⁍ is |
s :← StringBuilder(7) |
toHtmlColor(s) |
return s.toString() |
end |
method toHtmlColor(Appendable appendable) → () ⁌
⁍ is |
“#” | .toString(appendable) |
red | .toHexString(appendable, 2) |
green | .toHexString(appendable, 2) |
blue | .toHexString(appendable, 2) |
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.
self.name name |
self.salary ← 0 |
⟦setter⟧ |
method salary(salary : 𝔻₆₄) → () |
⟦getter⟧ |
method salary() → 𝔻₆₄ is |
⟦override⟧ |
method toString(Appendable appendable) → () is |
name | .toString(appendable) |
“: ” | .toString(appendable) |
salary | .toString(appendable) |
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.
- public: Jedes Objekt darf auf dieses Element zugreifen.
- protected: Nur abgeleitete Objekte dürfen auf dieses Element zugreifen.
- package: Nur Objekte aus dem selben Paket dürfen auf dieses Element zugreifen.
- private: Nur das Objekt selbst darf auf dieses Element zugreifen.
- privileged: Nur priviligierte Objekte dürfen auf dieses Element zugreifen.
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.
⟦privileged(owner: Container)⟧ |
p : Point |
… |
object Container isa Component is |
children : MutableList⟦Component⟧ |
method add(Component c) → () is |
c.p ← Point(…) |
children.add(c) |
Schnittstellen
⟦immutable⟧ |
field red | : ℕ₃₂{0, … , 255} |
⟦immutable⟧ |
field green | : ℕ₃₂{0, … , 255} |
⟦immutable⟧ |
field blue | : ℕ₃₂{0, … , 255} |
⟦convenience⟧ |
method toHtmlColor() → 𝕊 ⁌
⁍ is |
s :← StringBuilder(7) |
toHtmlColor(s) |
return s.toString() |
method toHtmlColor(Appendable appendable) → () ⁌
⁍ is |
“#” | .toString(appendable) |
red | .toHexString(appendable, 2) |
green | .toHexString(appendable, 2) |
blue | .toHexString(appendable, 2) |
c : Color ← … |
… c.red … | Liefert den Rot-Anteil der Farbe unanhägig von der konkreten Implementierung. |
object RgbColor isa Color ⁌
⁍ is |
init (r : 𝕌₈, g : 𝕌₈, b : 𝕌₈) is |
self.r | ← r |
self.g | ← g |
self.b | ← b |
⟦getter⟧ |
method red() → ℕ₃₂{0, … , 255} is |
⟦getter⟧ |
method green() → ℕ₃₂{0, … , 255} is |
⟦getter⟧ |
method blue() → ℕ₃₂{0, … , 255} is |
object RgbColor isa Color ⁌
⁍ is |
⟦private⟧ |
⟦immutable⟧ |
rgb : 𝕌₃₂ |
⟦public⟧ |
init (r : 𝕌₈, g : 𝕌₈, b : 𝕌₈) is |
rgb ← (((r ⋉ 8) ⦶ g) ⋉ 8) ⦶ b |
⟦getter⟧ |
method red() → ℕ₃₂{0, … , 255} is |
⟦getter⟧ |
method green() → ℕ₃₂{0, … , 255} is |
return (rgb ⋊ 8) ⊙ (FF)₁₆ |
⟦getter⟧ |
method blue() → ℕ₃₂{0, … , 255} is |
object RgbColor isa Color ⁌
⁍ is |
⟦private⟧ |
⟦immutable⟧ |
rgb : 𝕌₃₂ |
⟦public⟧ |
init (r : ℕ₃₂{0, … , 255}, g : ℕ₃₂{0, … , 255}, b : ℕ₃₂{0, … , 255}) is |
rgb ← (((r ⋅ B) + g) ⋅ B + b |
⟦getter⟧ |
method red() → ℕ₃₂{0, … , 255} is |
⟦getter⟧ |
method green() → ℕ₃₂{0, … , 255} is |
return emod(ediv(rgb, B), B) |
⟦getter⟧ |
method blue() → ℕ₃₂{0, … , 255} is |
object HsvColor extends RgbColor is |
typealias Degree 𝔽₃₂{x ∣ x ∈ [0 ; 360.0[}[°] |
typealias Percent 𝔽₃₂{x ∣ x ∈ [0 ; 1]}[%] |
⟦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.hue ← hue |
self.saturation ← saturation |
self.value ← value |
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 − sf) |
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.0̇⌋) |
⟦public⟧ |
method toDebugString(Appendable appendable) → () |
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) |
interface Debugable isa 𝕆 is |
⟦private⟧ |
method appendNameAndId(Appendable appendable) → () is |
getClass() | .toString(appendable) |
“@” | .toString(appendable) |
identityHashCode(self) | .toString(appendable) |
⟦public⟧ |
method toDebugString(Appendable appendable) → () is |
appendNameAndId((appendable) |
self.toString(appendable) |
Vererbung
Während die Vererbung bei Schnittstellen inhärent ist, muss sie bei Objekten explizit möglich gemacht werden.
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 : ℕ) → ℕ ⁌
⁍ is |
d :← x mod 10 |
x ← x ÷ 10 |
sum ← sum + d |
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⟦ℕ₃₂⟧) → () ⁌
⁍ is |
Ü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. |
m ← Mutable() |
test “Objektreferenz wird durch aufgerufene Routine nicht verändert” is |
m :← Mutable() |
old_reference :≡ m | Referenzwert speichern |
modify(m) |
assert m ≡ old_reference | Referenzwert („Adresse“) ist unverändert. |
Referenzübergabe – call by reference
test “Variable muss bei Variablenübergabe initialisiert sein” is |
⟦compiler_fail⟧ |
refVar(⟦ref⟧ n) | ↯ n muss initialisiert sein! |
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 : ℕ) → () ⁌
⁍ is |
procedure refVar(⟦ref⟧ v : ℕ) → () is |
test “Initialisierung via Variablenübergabe” is |
n : ℕ |
initVar(⟦var⟧ n) |
assert n = 5 |
refVar(⟦ref⟧ n) |
assert n = 6 |
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 p ≠ nil ⩑ p.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. |
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 : ℕ, ⟦lazy⟧ x : ℕ) → ℕ is |
total ← total + x | x wird höchstens ein einziges Mal ausgewertet. |
end |
x wurde bei n = 0 kein Mal ausgewertet |
return total |
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 {from ≤ to, δ > 0} |
list :← ListBuilder⟦ℕ₃₂⟧() |
loop |
init | i :← from |
while | i ≤ to |
step | i ← i + δ |
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) |
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 {offset ≤ offset + length < ∣a∣} |
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” |
Benannte Parameter
fixed : ℕ, |
fixed_optional : ℕ = 10, |
“named_optional” : ℕ = 20, |
“named_required” : ℕ, |
renamed_optional “x” : ℕ = 30 |
return (fixed, fixed_optional, named_optional, named_required, renamed_optional) |
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) |
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∗ : ℕ) → List⟦ℕ⟧ is |
list :← ListBuilder(∣elements∣) |
for e ∈ elements do |
assert makeList(1, 2, 3, 4, 5) = ⟨1, 2, 3, 4, 5⟩ |
function ⟪.⟫ : (elements∗ : ℕ) → List⟦ℕ⟧ is |
return makeList(∗elements) |
assert ⟪1, 2, 3, 4, 5⟫ = ⟨1, 2, 3, 4, 5⟩ |
function switch(i : ℕ, ⟦lazy⟧ expressions+ : ℤ) → ℤ |
requires {1 ≤ n ≤ ∣expressions∣} |
assert switch(2, ack(3, 10), 10, ack(3, 20)) = 10 |
function ack(n : ℕ, m : ℕ) → ℕ is |
◼ (0, m): |
return ack(n − 1, ack(n, m − 1)) |
Tests
return h() | ↯ Kann nur in Test-Funktionen verwendet werden. |
o ← O() |
assert o.f() = 1 |
assert o.h() + o.i() = 3 | Dürfen natürlich in einem Test verwendet werden. |
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 |
test div⦅x : ℤ₃₂, y : ℤ₃₂⦆ → ℤ₃₂ is |
◻ (2, 2) | ↦ 1 |
◻ (2, 1) | ↦ 2 |
◻ (2, 0) | throws ArithmeticException |
end |
Liefert die Anzahl der Ziffern einer nicht negativen, ganzen 16-Bit-Zahl.