Lua Tutorial (Stefan Bosse) [4.2025] |
In diesem Tutorial sollen grundlegendene Kenntnisse in der Programmierung mit der Programmiersprache Lua erworben werden.
Ein Programm in Ausführung wird in Daten- und Kontrollfluss unterteilt. Die prozedurale Programmiersprache Lua besteht aus eine Menge von Programmieranweisungen die man in folgende Klassen unterteilen kann:
Berechnungen (Datenfluss) bestehen wiederum aus folgenden Komponenten:
Eine Variable kann
In den folgenden Beispielen und Aufgaben können Werte und Variableninhalte mit der print
Funktion in dem unter dem Code befindlichen Ausgabefenster ausgegeben werden. Hat man komplexere Werte, wie z.B. Datenstrukturen und Arrays, kann man stattdessen die inspect
Funktion verwenden.
Dieses Tutorial ist in Snippets unterteilt. Ein Snippet besteht aus einem Texteditorfenster und einem darunter befindlichen Kontroll- und Ausgabefenster. Das Kontrollfenster hat links den Run Knopf, der den Code ausführt, und rechts ein Erase Knopf, der das Ausgabefenster löscht.
Alle Codesnippets werden in einem sogenannten globalen Kontext ausgeführt, d.h., praktisch bilden sie ein Programm! Einzelne Codesnipptes können von der Ausführung vorheriger abhängen.
Daher bei einem Fehler prüfen ob die vorherigen Codesnipptes fehlerfrei ausgeführt wurden!
Alle Eingabefelder (Code und Antworten auf Fragen) können gespeichert und wieder in das interaktive Tutorial geladen werden (Linker und rechter Knopf in der Kopfzeile).
Die Inhalte von Programmcodefeldern können über ein WEB Clipboard mit anderen Teilnehmern geteilt werden (wenn sie gleichzeitig am Tutorial arbeiten). Dazu befindet sich rechts oben in einem Codeeditorfeld ein Kopfsymbol. Das WEB Clipboard Fenster ist spezifisch für dieses Codefeld! Der Copy Befefhl fügt den eigenen Programmcode zum Clipboard hinzu. Code anderer Nutzer kann aus der Liste durch öffnen des Spoiler importiert werden. Der aktuell ausgwählte Code kann durch den Paste Befehl übernommen werden. Code Schnippsel verschwinden nach einer gewissen Zeit wieder aus dem Clipboard (ca. 5 Minuten).
Der Lua Interpreter ist in diese WEB App integriert. Es werden keine externen Programme benötigt! Der Zustand des Interpreters kann momentan nur durch erneutes Laden der Seite zurückgesetzt werden (vorher alle Eingaben speichern).
In Lua werden einzelne Anweisungen entweder durch einen Zeilenumbruch oder ein Semikolon ;
innerhalb einer Zeile getrennt. Ansonsten ist das Semikolon "syntaktischer Zucker". Einzelne Anweisungen werden immer in der angegebenen Reihenfolge ausgeführt (imperative Programmierung).
Weitere Informationen über Lua gibt es hier:
http://edu-9.de/uploads/documents/lua_tutorial.pdf
Lua ist dynamisch typisiert, d.h., Datentypen von Werten und Ausdrücken werden erst zur Laufzeit bestimmt. Werte und Ausdrücke können über den =
Operator einer Variablen zugewiesen werden. Lua kennt keine statische Typisierung, d.h., der Variablentyp ist polymorph und wird ausschließlich durch den aktuellen Inhalt bestimmt, der sich zur Laufzeit ändern kann.
Füge in den folgenden Code die print
Anweisung für y, z und f ein und beobachte die Ausgabe.
▸
|
✗
≡
|
Die Print Funktion kann wie üblich mit mehreren Argumenten aufgerufen werden.
Ausdrücke bestehen aus Operanden (können auch wieder Ausdrücke sein) und Operatoren, die Variablen, Funktionen oder Werte sein können:
+
-
*
/
%
and
or
not
math.sin()
usw.false
und true
"text"
▸
|
✗
≡
|
Aufgabe 1. Was passiert wenn man verschieden Datentypen in Ausdrücken verwendet, also z.B. Addition x+y
und Boolesche Verknüpfung x and y
? Probieren Sie alle Kombinationen (Ganzzahlwert, Rationale Werte, Boolescher Wert, Zeichenketten). Was geht, was nicht?
▸
|
✗
≡
|
Aufgabe 2. Da es keine Datentypisierung im Programmtext gibt müssen (numerische) Werte von einem festgelegten Datentyp sein, z.B. Integer, Float (einfache Präzision, Mantisse , Double (doppelte Fließkommapräzision). Finde mit obigen Operatoren heraus welchen numerischen Datentyp diese Lua Implementierung verwendet.
▸
|
✗
≡
|
eD0xLzIKcHJpbnQoeCkgLS0gMC41LCBhbHNvIEZsaWVzc2tvbW1hCng9MS8zCnByaW50KHgpIC0tIDAuMzMzMzMzMzMzMzMzMzMsIGFsc28gd29obCBEb3VibGUKeD0xRTMwICAgLS0gU2luZ2xlL0RvdWJsZSBtw7ZnbGljaApwcmludCh4KQp4PTFFMzAwICAKcHJpbnQoeCkgLS0gTnVyIGJlaSBEb3VibGUgbcO2Z2xpY2gKeD0xRTkwMApwcmludCh4KQotLSBFaW5mYWNoZSBQcsOkemlzaW9uOiBNPTIzL0U9OCBCaXRzCi0tIERvcHBlbHRlIFByw6R6aXNpb246IE09NTIvRT0xMSBCaXRz
Man unterscheidet globale und lokale Variablen. Eine globale Variable wird einfach durch eine Zuweisungsanweisung x=v
eingeführt. Eine lokale Variable wird durch die Anweisung local x=v
eingeführt. Der Initialwert v ist optional. In Funktionen immer nur lokale Variablen definieren!
Aufgabe 3. Welchen Nachteil hat ein einheitlicher numerische Datentyp (bedenke auch Kontrollanweisungen wie inkrementelle Schleifen)?
▸
|
✗
≡
|
Aufgabe 4. Gebe in obigen Beispiel den Wert von x1 in foo2 aus. Was passiert? Und wenn x2 ausgegeben wird? Welchen Wert hat die globale Variable x1 nach der Ausführung von foo1? Ändere die äußere globale Definition von x1 an dieser Stelle in eine lokale (löcal x1=100
) um. Was passiert?
100 40804 22 11
, d.h., in foo1 gibt es eine überdeckte Variable x1 als Funktionsparameter, in foo2 wird die globale Variable x1 verändert.Lokale Variablen können in Funktionen aber auch in Anweisungsblöcken (Schleifen, Bedingte Verzweigung) definiert und benutzt werden.
Steueranweisungen dienen zur Beeinflussung des Programmflusses. Man unterscheidet (mit s
als Anweisung oder einer Reihe von Anweisungen):
if e then s1 else s2 end
while e do s end
for i=a,b do s end
oder mit einer Schrittweite d for i=a,b,d do s end
Folgendes Beispiel zeigt die Implementierung der Funktion:
▸
|
✗
≡
|
Der Programmfluss von Schleifen ist in folgender Abbildung dargestellt (Flussdiagramme).
Aufgabe 5. Im folgenden Beispiel wird mit einer Turtlegraphik ein Oktagon gezeichnet. Ersetze den Code mit einer Zählschleife die die gleiche Figur zeichnet und in Winkelschritten zählt.
turtle:forward(n)
moves the turtle in current direction n
pixels forwardturtle:goTo(x,y)
moves the turtle to a new positionturtle:left(n)
turns turtle n
degree leftturtle:right(n)
turns turtle n
degree rightturtle:reset()
clears the drawing areaturtle:up()
activates the penturtle:down()
deactivates the pen
▸
|
✗
|
|
dHVydGxlOnJlc2V0KCkKCmZvciBhID0gMCwzNjAsNDUgZG8KICB0dXJ0bGU6Zm9yd2FyZCg1MCkKICB0dXJ0bGU6cmlnaHQoNDUpCmVuZA==
In Lua gibt es keine Anweisung für die Mehrfachauswahl, d.h., der Vergleich eines Ausdrucks mit verschiedenen Werten und konditionaler Ausführung von Fallanweisungen. Erdatz ist eine if-elsif Kette, aber nicht schön. Es gibt eine funktionale Erweiterung über eine Falltabelle (in lvm usw. bereits enthalten):
Switch(expr).caseof({
[v1] = function,
[v2] = function,
...
[v3] = v1,
default = function
})
Switch
erzeugt einen Selektor für den Ausdruck expr, der mit der caseof
Methode mit konstanten Werten vi verglichen wird, und bei Übereinstimmung die Fallfunktion aufruft. Mehrere Werte v1,v2,.. können an einen einzigen Fall v mittels [v1]=v
gebunden werden.
▸
|
✗
≡
|
Frage 6. Was passiert wenn die Anweisung mit x=3 ausgeführt wird?
Frage 7. Funktiioniert die Mehrfachauwahl auch mit Zeichenketten? Implementiere einen Wortdekoderfunction execute, die die Worte "add", "sub", "mul" und "div" dekodiert und die beien Operanden a und b entsprechend berechnet und das Ergebnis zurückgibt. Kann die Switch Funktion die Werte der Fallfunktionen zurückgeben? Teste mit Beispielen.
▸
|
✗
≡
|
ZnVuY3Rpb24gZXhlY3V0ZShhLG9wLGIpIAogIHJldHVybiBTd2l0Y2gob3ApLmNhc2VvZih7CiAgICBbImFkZCJdPWZ1bmN0aW9uICgpIHJldHVybiBhK2IgZW5kLAogICAgWyJkaXYiXT1mdW5jdGlvbiAoKSByZXR1cm4gYS9iIGVuZCwKICAgIFsibXVsIl09ZnVuY3Rpb24gKCkgcmV0dXJuIGEqYiBlbmQsCiAgICBbInN1YiJdPWZ1bmN0aW9uICgpIHJldHVybiBhLWIgZW5kCiAgfSkKCmVuZApwcmludChleGVjdXRlKDEwLCJhZGQiLDIwKSk=
local function Switch(c)
local swtbl
swtbl = {
casevar = c,
caseof = function (code)
local f
if swtbl.casevar ~= nil then
f = code[swtbl.casevar] or code.default
else
f = code.missing or code.default
end
if type(f) == "string" then
f = code[f] or code.default
elseif type(f) == "number" then
f = code[f] or code.default
end
if f then
if type(f)=="function" then
return f(swtbl.casevar,self)
else
error("case "..tostring(swtbl.casevar).." not a function ("..type(f)..")")
end
end
end
}
return swtbl
end
Switch=switch
Funktionen dienen der Programmkomposition und der Wiederverwendung von Code. Man unterscheidet:
Eine Funktionsdefinition besteht aus:
return
zurückgegeben wird.
Die Applikation ist einfach: Name der Funktion mit folgender Liste von Argumenten, d.h., foo(1,2,3)
. Füge in folgenden Beispielen Funktionsaufrufe (Applikationen) von foo und foo2 ein und gebe die Werte mit print aus.
▸
|
✗
≡
|
Frage 8. Was passiert wenn die Anzahl der Argumente kleiner oder größer als die Anzahl der Funktionsparameter ist? Teste es mit foo und foo2.
Frage 9. Was ist Funktionsrekursion?
Wenn eine Funktion sich in ihrem Funktionsrumpf selbst aufruft!
Wende die Funktionen fac und facsq für die Werte 1,10,20 an und gebe die Werte aus. Ein kleiner Tipp: Obwohl Lua eigentlich nur Fliesskommawerte (quasi reelle Zahlen) verarbeitet (und keine ganzen), unterscheidet der Lua Interpreter der in diese WEB App integriert ist (aus einem mir nicht bekannten Grund) ganze Zahlen (Integer, 1,2,3,..) und Fliesskommazahlen (Float, 1.0, 1.1, 1.2, ..). Probiere einmal als Wert 10
und dann 10.0
...
▸
|
✗
≡
|
Frage 10. Wie kann die Berechnung von facsq optimiert werden (Annahme: Berechung von fac ist rechenintensiv!). Messe die Rechenzeit durch wiederholte Ausführung von facsq und mittels der os.milli()
Funktion. Was ist bei der Messung zu beobachten?
LS0gTHVhIG5pbW10IGkuQS4ga2VpbmUga29tcGxleGVuIEF1c2RydWNrc29wdGltaWVydW5nZW4gdm9yIChhbmRlcnMgYWxzIHouQi4gSGFza2VsbCkKZnVuY3Rpb24gZmFjKG4pCiAgaWYgbiA+IDEgdGhlbiByZXR1cm4gbipmYWMobi0xKQogIGVsc2UgcmV0dXJuIG4gZW5kCmVuZApmdW5jdGlvbiBmYWNzcShuKQogIGxvY2FsIHk9ZmFjKG4pCiAgcmV0dXJuIHkqeQplbmQKbG9jYWwgdDA9b3MubWlsbGkoKQpmb3IgaT0xLDEwMCBkbwogIGZhY3NxKDIwKQplbmQKbG9jYWwgdDE9b3MubWlsbGkoKQotLSBEaWUgWmVpdCBzY2h3YW5rdCEgV2FydW0/CnByaW50KHQxLXQwKQ==
Aufgabe 11. In der folgenden Aufgabe soll die Fibonacci Funktion implementiert und getestet werden. Benutze Funktionsrekursion! Es sollen alle Fibonacci Zahlen für n={1,2,3,4,..,20} berechnet und ausgegeben werden.
▸
|
✗
≡
|
ZnVuY3Rpb24gZmliKG4pCiAgaWYgbj4xIHRoZW4KICAgIHJldHVybiBmaWIobi0xKStmaWIobi0yKQogIGVsc2UKICAgIHJldHVybiAxCiAgZW5kCmVuZApmb3Igbj0xLDIwIGRvCiAgcHJpbnQoZmliKG4pKQplbmQ=
Variablen in Lua sind polymorph. Somit auch Funktionen, d.h. deren Funktionsparameter und der Rückgabewert! Es gibt nur wenige Grunddatentypen in Lua:
That's all folks!
Der Datentyp eines Wertes (einer Variablen) kann mit der type
Funktionen ermittelt werden (gibt einen Stringwert zurück).
▸
|
✗
≡
|
Aufgabe 12. Teste und gebe die Datentypen der Werte von x, y, und sq mit der print(type(v))
Anweisung aus.
Ja richtig erkannt: Funktionen sind Werte in Lua!!! Funktionen können in Variablen enthalten sein, an Funktionen als Argument übergeben werden, und von Funktionen als Wert zurückgegebn werden!
Da Funktionen auch Werte (erster Ordnung) sind, kann man damit viele Berechnungen vereinfachen bzw. abstrahieren.
Beispiel ist die Berechnung eines bestimmten Integrals einer beliebigen Funktion. Man müsste für jede Funktion die Integralberechnung immer wieder definieren. Es geht auch anders:
▸
|
✗
≡
|
Aufgabe 13. Füge eine Integralfunktion isqx für die Quadratfunktion x • x und isinx für die Sinusfunktion math.sin
hinzu und teste exemplarisch.
▸
|
✗
≡
|
aXNxeD1pbnRlZ3JhbChmdW5jdGlvbiAoeCkgcmV0dXJuIHgqeCBlbmQpCmlzaW54PWludGVncmFsKGZ1bmN0aW9uICh4KSByZXR1cm4gbWF0aC5zaW4oeCkgZW5kKQ==
Um Daten namentlich zu binden verwendet man Arrays und Records (Datenstrukturen). Der Unterschied zwischen beiden ist gering. Die Elemente von Arrays werden über einen numerischen Index (1,2,..) ausgewählt, die von Records durch die vom Programmierer gegebenen Namen der Elemente. Anders als in Programmiersprachen mit statischen Typsystem (C, Java) können auch Arrays in Lua mehrsortig sein, d.h. die Elemente können unterschiedliche Datentypen haben!
Arrays und Records sind in Lua sog. Tabellen. Genau genommen sind es beide Hashtabellen. Arrays und Records werden in Lua wie folgt definiert (vi sind Werte oder Ausdrücke):
-- Array
local a = {v1,v2,..,vn}
-- Record
local r = {e1=v1, e2=v2, .. , en:vn}
Es gibt zwei Arten auf die Elemente von Records zuzugreifen:
x=r['ei']
x=r.ei
Die Länge eines indizierten Arrays kann durch den #a
Operator ermittelt werden (d.h. die Anzahl der Elemente).
Folgende Beispiele sollen dies verdeutlichen:
▸
|
✗
≡
|
Frage 14. Welche Werte haben die einzelnen Variablen axi und rxi? Lese nach was Hashtabellen sind (Wikepedia https://de.wikipedia.org/wiki/Hashtabelle) und erkläre die Ergebnisse.
Arrays und Records gehören zur prozeduralen Programmierung. D.h., man definiert Arrays/Records getrennt von Funktionen die die Arrays/Records verarbeiten. Beispiele:
▸
|
✗
≡
|
Über Tabellen (Indizierte Arrays wie Records) kann mittels Schleifen und des k,v in pairs(t)
oder i,v in ipairs(t)
Operators iteriert werden. Der Paaroper pairs liefert Schlüssel (k ist eine Zeichenkette), und ipairs einen Index (i ist ein numerischer Wert). Zu beachten ist dass die Paaroperatoren rechenintensiver sind als dir direkte Indizierung.
▸
|
✗
≡
|
Frage 15. Was ist das Problem bei der getrennten Programmierung von Daten und Funktionen die auf diesen Daten arbeiten?
Betrachte folgendes Beispiel:
▸
|
✗
≡
|
Aufgabe 16. Implementiere die Berechnung eines Skalarprodukts zweier zweidimensionaler Matrizen a und b die durch lineare Arrays gegeben sind. Dabei muss eine Indexrechung durchgeführt werden: Wenn N die Anzahl von Zeilen und Spalten der Matrizen ist, dann ist der lineare Index für Index(row,col)=(row-1)*N+col.
▸
|
✗
≡
|
bG9jYWwgTiA9IDIwCmxvY2FsIGEgPSBBcnJheShOKk4sMCkKLi4uCmZ1bmN0aW9uIGRvdHByb2QoYSxiKSAKICBsb2NhbCBjID0gQXJyYXkoI2EsMCkKICBmb3IgaT0wLE4tMSBkbwogICAgZm9yIGo9MCxOLTEgZG8KICAgICAgbG9jYWwgcz0wCiAgICAgIGxvY2FsIGNpID0gTippK2oKICAgICAgZm9yIGs9MCxOLTEgZG8KICAgICAgICBsb2NhbCBhaSA9IE4qaStrIAogICAgICAgIGxvY2FsIGJpID0gTiprK2oKICAgICAgICBzPXMrYVthaSsxXSpiW2JpKzFdCiAgICAgIGVuZAogICAgICBjW2NpKzFdPXMKICAgIGVuZAogIGVuZAogIHJldHVybiBjCmVuZA==
Objekte binden Daten und Funktionen (hier genannt Methoden). Die Methoden arbeiten nur auf Daten ihres Objekts. Objekte werden von Klassenprototypen erzeugt.
In Lua gibt es keine Klassen und somit auch keine Objekte. Diese müssen mit Tabellen "emuliert" werden, d.h. die Bindung von Prototypenfunktionen an Tabellen.
Da Funktionen Werte sind kann man erst einmal Records (also Tabellen) direkt verwenden. Ist das schon ein Objekt nach obiger Definition?
▸
|
✗
≡
|
Da die Bindung von Prototypenfunktionen an Tabellen etwas kompliziert ist gibt es eine einfache Funktion um eine leere Klasse als Tabelle zu definieren: Class()
. Es können dann "Methoden" (also Funktionsprototypen) hinzugefügt werden und beliebig viele Objekte von dieser Klasse erzeugt werden:
▸
|
✗
≡
|
Erweitere die Klasse um die Methoden incr
und decr
die die Objektvariable x um eins erhöht oder erniedrigt. Teste die Methoden exemplarisch.
Zu beachten ist der Doppelpunkt zwischen Klassenname und Methodenname! Dieser sorgt für eine Bindung der Funktion an das Objekt!
local function class()
local Class = {}; Class.__index = Class;
function Class:new(...)
local self = setmetatable({}, Class)
if Class.init then Class.init(self,...) end
return self
end
return Class
end
Class=class
Aufgabe 17. Programmiere eine Matrix Klasse die (zweidimensionale) Matrizen mit linearen Arrays erzeugen kann. Weiterhin sollen die Operationen elemntweises addieren, subtrahierne, multiplizieren, und dividieren sowie das oben schon implementierte Skalarprodukt als Methode hinzugefügt werden.
▸
|
✗
≡
|
In einem Programm können Fehler auftreten:
x[9], x.attr
)Ein Fehler löst ein Signal aus (ein Ausnahmesignal). Wird dieses Signal nicht behandelt, führt dies zum Abbruch des Programms (i.A.). Es gibt aber auch Signale die man verwendet um an einer Stelle des Programms an eine andere "höhergelegene" Stelle zu gelangen. Entweder geplant oder auch um "seitwärts" aus einer Funktion bei einem Fehler (nicht schwerwiegend) wieder zum aufrufenden Kontext zu gelangen.
Programmiersprachen wie Java oder JavaScript bieten Signalbehandlung mittels einer try catch Anweisung, Lua leider nicht direkt/komfortabel (nur mittels der pcall
oder xpcall
Funktionen).
Ein Signal kann in Lua durch die raise(signal)
Anweisung ausgelöst werden. Das Signal ist i.A. ein Stringname.
▸
|
✗
≡
|
Frage 18. Was passiert wenn die Funktion ohne Argument aufgerufen wird? Wie ändert sich das Verhalten bei Aufruf von incr
durch pcall(function () .. end)
? Was gibt pcall als Wert zurück (mit print testen)? Welche Wert lifert status?
Man kann die fehlende try-catch Anweisung aber funktional ergänzen, und man erhält dann:
▸
|
✗
≡
|
local functionType = "function"
local function Try (tryBlock)
local status, err = true, nil, trace
function traceback(e)
trace = e;
return e
end
if type(tryBlock) == functionType then
status, err = xpcall(tryBlock,traceback)
end
local _finally = function (finallyBlock, catchBlockDeclared)
if type(finallyBlock) == functionType then
finallyBlock()
end
if not catchBlockDeclared and not status then
raise(err)
end
end
local _catch = function (catchBlock)
local catchBlockDeclared = type(catchBlock) == functionType;
if not status and catchBlockDeclared then
local ex = err or "unknown error occurred"
catchBlock(ex,trace)
end
return {
finally = function(finallyBlock)
_finally(finallyBlock, catchBlockDeclared)
end
}
end
return
{
catch = _catch,
finally = function(finallyBlock)
_finally(finallyBlock, false)
end
}
end
Try=try