Programmieren lernen mit Lua: Lektion 04: Funktionen selber schreiben

Übersicht | Vorherige Lektion | Nächste Lektion





Wir haben bereits ab der ersten Lektion Funktionen verwendet:

print()
type() 
io.read()
tonumber()
math.random()

Du kannst Dir eine Funktion wie eine Portion Code vorstellen, die Du zum wiederholten Ausführen zusammenfasst. Genau so gut und richtig ist die Idee, eine Funktion als ein Unterprogramm zu sehen, das auf eine bestimmte Aufgabe spezialisiert ist. Anstatt an jeder Stelle, in der Du ihn brauchst, den kompletten Code hinzuschreiben, kannst Du ihn in eine Funktion verpacken und diese Funktion dann an beliebigen Stellen in Deinem Programm verwenden.

Die Verwendung einer Funktion wird oft Funktionsaufruf genannt. Ein Funktionsaufruf besteht im Normalfall aus dem Namen der Funktion und einem Paar runder Klammern. Zwischen den Klammern stehen oft ein oder mehrere Argumente. Argumente sind Werte, welche genauer beschreiben, was die Funktion machen soll.

> print("guten tag")
guten tag

In diesem Fall ist der Name der Funktion print(). Das Argument ist der String "guten Tag".

Manche Funktionen können auch mehrere Argumente verarbeiten, das gilt auch für print():

> print("dies", "das", "jenes")
dies das jenes

Andere Funktionen benötigen kein Argument, zum Beispiel io.read(). Die runden Klammern gehören trotzdem immer zum Funktionsaufruf:

eingabe = io.read()

Die runden Klammern unterscheiden den Funktionsaufruf von der Funktion selbst:

> type(print)
function
> type(print())
nil

io.read() ist eine Funktion, die einen Wert zurückliefert. Dazu werden wir später noch einiges sagen.

Eine Pizzeria programmieren

Stell Dir vor, Du möchtest ein Textadventure programmieren, in dem es um den Betrieb einer Pizzeria geht. Jedes Mal, wenn ein Kunde eine Pizza bestellt, soll das Programm den Arbeitsablauf der Zubereitung beschreiben. Eine erste Variante dieses Programmteils könnte so aussehen:

print("Teig ausrollen")
print("Tomatensoße hinzu")
print("geriebenen Käse hinzu")
print("backen") 

Das Programm auf repl.it

Die Pizza per Funktion zubereiten

Es ist ein leichtes, aus diesem Code eine Funktion zu bauen:

function pizza_backen()
  print("Teig ausrollen")
  print("Tomatensoße hinzu")
  print("geriebenen Käse hinzu")
  print("backen")
end

Das Schlüsselwort function bedeutet: hier kommt eine Funktionsdefinition. pizza_backen ist der Name der Funktion, die hier definiert wird. Die leeren runden Klammern zeigen an, dass die neue Funktion keine Argumente hat. Die darauf folgenden, eingerückten Zeilen bis zum end ist der Code, der ausgeführt werden soll, wenn jemand die Funktion aufruft. Wir sprechen auch hier (genau wie bei der bedingten Ausfühung per if oder bei Schleifen per while und for) von einem Block. Bei Funktionen spricht man auch oft vom Körper der Funktion. Auch hier ist eine Einrückung des Körpers üblich.

Wenn Du das letzte Beispiel ausführst, passiert erst einmal nichts. Du hast ja die Funktion lediglich definiert. Ein Pizzarezept ist noch keine Pizza! Um die Funktion zu verwenden, schreibst Du ganz einfach:

pizza_backen()

Das Programm auf repl.it

In diesem Fall definieren wir die Funktion in der Datei main.lua, verwenden sie aber im interaktiven Modus auf der Konsole:

Aufgabe 1: Schreibe eine eigene Funktion tee_kochen(), welche die Zubereitungsschritte einer Tasse Tee in den drei Schritten „Wasser kochen“, „Teebeutel in Tasse hängen“ und „Wasser aufgießen“ nach dem Muster unserer Pizza-Funktion beschreibt. Führe die Funktion drei Mal aus. Wenn Du keinen Tee magst, kannst Du auch gerne ein anderes Rezept oder eine Bauanleitung verwenden!

function tee_kochen()
  print("Wasser kochen")
  print("Teebeutel in Tasse hängen")
  print("Wasser aufgießen")
end

tee_kochen()
tee_kochen()
tee_kochen()

Die Lösung auf repl.it

Du kannst bereits an dem Pizza Beispiel sehen: Der Code innerhalb der Funktion (hier: pizza_backen) kann wiederum andere Funktionen (hier: print()) aufrufen. Selbstverständlich können auch selbst geschriebene Funktionen wiederum selbst geschriebene Funktionen aufrufen. Bei der Entwicklung komplexerer Programme besteht ein Gutteil der Arbeit darin, ein erstmal schwer überschaubares Problem in überschaubare, leicht verständliche Funktionen zu zerlegen.

Aufgabe 2: Schreibe eine Funktion teig_zubereiten(), welche die Beschreibung der Zubereitung eines Pizza Teiges ausgibt:

Wasser, Mehl, Hefe, Öl und Salz in Schüssel geben
Zutaten rühren
Teig gehen lassen

Rufe diese Funktion innerhalb der Funktion pizza_backen() auf, bevor der Teil ausgerollt wird.

function teig_zubereiten()
  print("Wasser, Mehl, Hefe, Öl und Salz in Schüssel geben")
  print("Zutaten rühren")
  print("Den Teig gehen lassen")
end

function pizza_backen()
  teig_zubereiten()
  print("Teig ausrollen")
  print("Tomatensoße hinzu")
  print("geriebenen Käse hinzu")
  print("backen")
end

Die Lösung auf repl.it

Die Pizzafunktion um ein Argument erweitern

Eine Pizzeria, in der es nur die Standart Margherita Pizza gibt, würde sich wohl nicht lange halten. Es sollte zum Beispiel auch Pizzen mit Zwiebeln oder Pilzen geben. Eine Möglichkeit wäre, für jede Sorte eine eigene Funktion zu schreiben:

function pizza_mit_pilzen_backen()
  print("Teig ausrollen")
  print("Tomatensoße hinzu")
  print("Pilze hinzu")
  print("geriebenen Käse hinzu")
  print("backen")
end

function pizza_mit_zwiebeln_backen()
  print("Teig ausrollen")
  print("Tomatensoße hinzu")
  print("Zwiebeln hinzu")
  print("geriebenen Käse hinzu")
  print("backen")
end

Die beiden Funktionen unterscheiden sich nur in einer einzigen Zeile. In der Softwareentwicklung gibt es eine Faustregel: Wenn ihr längere identische Codeabschnitte mehrfach in eurem Code stehen habt, dann solltet ihr überlegen, wie ihr den wiederholten Code zusammenfassen könnt. Im gegebenen Fall ist das sehr einfach: Wir spendieren der Funktion ein Argument extrabelag:

function pizza_backen(extrabelag)
  print("Teig ausrollen")
  print("Tomatensoße hinzu")
  print(extrabelag .. " hinzu")
  print("geriebenen Käse hinzu")
  print("backen")
end

Das Programm auf repl.it

Das geht ganz einfach, wie das Beispiel zeigt: im Upgrade unserer Pizza-Funktion steht jetzt ein Bezeichner, eben extrabelag. Das zeigt an, dass diese Funktion ein Argument verarbeiten kann. Wenn die Funktion zum Beispiel mit dem Argument "Pilze" aufgerufen wird – pizza_backen("Pilze") – dann wird überall dort, wo im Körper der Funktion der Bezeichner extrabelag auftaucht durch den Wert "Pilze" ersetzt. Das Argument bestimmt den veränderlichen Teil der Funktion.

Mit dieser Funktion könnt ihr nunmehr Pizzen mit beliebigen Extrabelägen backen:

pizza_backen("Spiegelei")
pizza_backen("Schuhsohle")
pizza_backen("Vanilleeis")

Es gibt an der neuen Funktion allerdings einen Haken: Wenn ihr sie ohne Argument aufruft, quittiert der Lua-Interpreter das mit einer Fehlermeldung:

$ pizza_backen()
attempt to concatenate local 'extrabelag' (a nil value)

Das Bedeutet auf deutsch: Versuch, den Wert extrabelag anzuhängen ist gescheitert, weil dieser Wert nil ist. Dieser Fehler passiert in der Zeile

print(extrabelag .. " hinzu")

Hintergrund dieses Fehlers ist: Wenn bei dem Aufruf unserer Funktion kein Argument verwendet wird, ist extrabelag schlichtweg nil. Und nil kann nicht mit dem String “ hinzu.“ verknüpft werden.

Aufgabe 3:
Hast Du eine Idee, wie wir diesen Fehler verhindern können? Es kann ja sein, dass jemand eine einfache Margherita ohne Extrabelag wünscht. Zwei Tipps: Du braucht eine bedingte Ausführung per if. Und Du machst Dir die Tatsache zu Nutze, dass nil wie false bewertet wird.

function pizza_backen(extrabelag)
  print("Teig ausrollen")
  print("Tomatensoße hinzu")
  if extrabelag then
    print(extrabelag .. " hinzu")
  end
  print("geriebenen Käse hinzu")
  print("backen")
end

Die Lösung auf repl.it

Aufgabe 4: Schreibe nach dem Muster der Lösung von Aufgabe 2 eine Funktion, die zwei Extrabeläge auf die Pizza bringen kann. Ein Hinweis dazu: mehrere Argumente werden einfach mit Kommas getrennt.

function pizza_backen(extrabelag, extrabelag2)
  print("Teig ausrollen")
  print("Tomatensoße hinzu")
  if extrabelag then
    print(extrabelag .. " hinzu")
  end
  if extrabelag2 then
    print(extrabelag2 .. " hinzu")
  end
  print("geriebenen Käse hinzu")
  print("backen")
end

Die Lösung auf repl.it

Funktionen mit Rückgabewert

Wir können Funktionen grob gesagt in zwei Kategorien einteilen:

  1. Funktionen, die etwas machen
  2. Funktionen, die einen Wert zurückliefern

Die Funktion print() und unsere selbstgestrickte Funktion pizza_backen() gehören zur ersten Kategorie. Sie geben etwas auf der Konsole aus.

Die Funktionen type() oder tonumber() gehören zur zweiten Kategorie: type() liefert den Typen des übergebenen Arguments als String. tonumber() versucht, einen übergebenen String in eine Zahl umzuwandeln:

> type(10)
number
> tonumber("42")
42
> tonumber("sieben")
nil
Die Funktion plus_sieben

Im folgenden zeigen wir, wie ihr selber Funktionen schreibt, die einen Wert zurückliefert. Das erste Beispiel ergibt praktisch nicht besonders viel Sinn, ist aber anschaulich. Die Funktion plus_sieben() soll zum Argument sieben hinzuzählen, und das Ergebnis zurückliefern:

function plus_sieben(x)
  return x + 7
end

Das, was hinter dem return steht, ist schlichtweg der Rückgabewert der Funktion.

Die Anwendung auf der Konsole sieht dann so aus:

$ plus_sieben(3)
10
$ plus_sieben(7)
14
$ plus_sieben(-5)
2

Das Beispielprogramm auf repl.it

Aufgabe 5: Schreibe nach dem Muster von plus_sieben() folgende drei Funktionen und teste sie auf der Konsole:
plus_drei(), minus_fuenf(), mal_zehn(), geteilt_durch_drei()

function plus_drei(x)
  return x + 3
end

function minus_fuenf(x)
  return x - 5
end

function mal_zehn(x)
  return x * 10
end

function geteilt_durch_drei(x)
  return x / 3
end

Die Lösung auf repl.it

Aufgabe 6: Schreibe eine Funktion, die true zurückliefert, wenn das Argument x eine Zahl (type(x) == "number") ist, sonst false. Teste die Funktion auf der Konsole.

function ist_zahl(x)
  return type(x) == "number"
end

Die Lösung auf repl.it

Anmerkung: Anfänger tendieren oft dazu, diese Lösung mit einem wesentlich komplizierteren Konstrukt in folgender Art zu lösen:

if type(x) == "number" then
  return true
else
  return false
end

Das ist zwar nicht falsch, aber zu umständlich. type(x) == "number" ist ja bereits das, was wir wissen und zurückliefern wollen.

Aufgabe 7: Schreibe eine Funktion ist_dazwischen(wert, von, bis), die true zurückgibt, wenn wert genau zwischen von und bis ist, sonst false. Teste die Lösung auf der Konsole.

function ist_dazwischen(wert, von, bis)
  return wert > von and wert < bis
end

Die Lösung auf repl.it

Mehrere returns in einer Funktion

In vielen Situationen ist es sinnvoll, mehrere returns im Körper einer Funktion stehen zu haben. Ein Beispiel ist die folgende Funktion begrenze(wert, von, bis). Sie gibt den wert unverändert zurück, wenn er zwischen von und bis liegt. Liegt der unter von, dann soll von zurückgegeben werden. Liegt wert über bis, soll bis zurückgegeben werden. Solche oder ähnliche Funktionen werden oft benötigt, wenn es etwa darum geht, in einem Spiel zu verhindern, dass ein Spielobjekt (ein Ball oder ähnliches) das Spielfeld verlässt.

$ begrenze(5, 0, 10)
5
$ begrenze(-3, 0, 10)
0
$ begrenze(20, 0, 10)
10

Der Code sieht so aus – hier haben wir drei Stellen, an denen der Funktionskörper per return verlassen werden kann:

function begrenze(wert, von, bis)

  if wert < von then
    return von
  end

  if wert > bis then
    return bis
  end

  return wert
  
end

Das Programm auf repl.it

return statt break

Vielleicht ist es Dir schon aufgefallen: return und break haben eine gewisse Ähnlichkeit: Während break den Sprung aus einer Schleife veranlasst, sorgt return für das Verlassen des Funktionskörpers. Tatsächlich kannst Du mit return auch eine Funktion verlassen, die keinen Rückgabewert hat. Und in manchen Situationen kannst Du ein return statt einem break verwenden. Beides zeigt das folgende Beispiel:

function wuerfel_bis_sechs()

  while true do
    zahl = math.random(6)
    print(zahl)

    if zahl == 6 then
      break
    end
  end

end

Das Programm auf repl.it

Die Funktion soll so lange „würfeln“ (= Zufallszahlen zwischen 1 und 6 erzeugen und auf der Konsole ausgeben), bis eine sechs kommt. In diesem Fall sorgt break für das Verlassen der while-Schleife und der Funktion.

Aufgabe 8 (für Fortgeschrittene):Schreibe eine Funktion zahl_eingeben(). Diese soll ungültige Eingaben (also Eingaben, die sich nicht durch tonumber() in eine Zahl umwandeln lassen) abfangen und die Nutzerin so lange zur Eingabe einer Zahl auffordern, bis eine gültige Eingabe kommt. Diese soll dann als Zahl zurückgeliefert werden. Speichere den Rückgabewert der Funktion und gib ihn aus.

Bitte gib eine Zahl ein.
$ sieben
sieben kann ich nicht als Zahl interpretieren.
Bitte gib eine Zahl ein.
$ Möhrensalat
Möhrensalat kann ich nicht als Zahl interpretieren.
Bitte gib eine Zahl ein.
$ 7
Du hast 7 eingegeben.

function zahl_eingeben()

  while true do
    print("Bitte gib eine Zahl ein.")
    eingabe = io.read()
    zahl = tonumber(eingabe)

    if zahl then
      return zahl
    else
      print(eingabe .. " kann ich nicht als Zahl interpretieren.")
    end
  end
  
end

x = zahl_eingeben()

print("Du hast " .. x .. " eingegeben.")

Die Lösung auf repl.it

Was wir hier ausgelassen haben

Auch zum Thema Funktionen lässt sich noch sehr viel mehr sagen, als wir in dieser Lektion präsentieren konnten. Einige weiter wichtige Themen wollen wir hier zumindest kurz skizzieren.

Anzahl von Argumenten und Rückagewerten

Lua erlaubt es Funktionen mit einer variablen Anzahl von Argumenten und Rückgabewerten zu schreiben. Für ersteres braucht man Tabellen, die wir erst in der nächsten Lektion kennenlernen. Mehrere Rückgabewerte könnt ihr auf folgende Weise zurückgeben und weiterverarbeiten:

function mehrere()
  return 1, 2, 3
end

a, b, c = mehrere()
Rekursive Funktionen

Es besteht die Möglichkeit, dass eine Funktion sich selbst aufruft. Das klingt verrrückt und würde im folgenden Beispiel auch die Funktion zu einer Endlosschleife und damit zu einem Absturz eures Programms führen:

function selbstaufrufer()
  selbstaufrufer()
end

Aber es gibt Situationen, in denen es sehr sinnvoll ist, solche Funktionen zu schreiben. Klassische Anwendungsbeispiele sind die Berechnung der Fibonacci-Zahlenfolge und die Tiefensuche.

Funktionen zweiter Ordnung

Bereits in der ersten Lektion haben wir gesehen: Auch Funktionen sind Werte – und zwar Werte vom Typ function:

> type(print)
function

Daher könnt ihr unter Bezeichnern Funktionen speichern wie jeden anderen Wert auch. Im folgenden sorgen wir dafür, dass der Bezeichner drucke auf die Funktion print verweist. Anschließend können wir drucke genau so wie print verwenden:

> drucke = print
> drucke("hallo")
hallo

Tatsächlich ist die Schreibweise, mit der wir in dieser Lektion Funktionen definiert haben, eine Abkürzung.

function gruss()
  print("guten tag!")
end

ist eigentlich

gruss = function()
  print("guten tag!")
end

Der Bezeichner ist keine Eigenschaft der Funktion, genau so wenig wie bei x = 3 der Bezeichner x eine Eigenschaft der Zahl 3 ist! x zeigt auf 3, genau so wie gruss auf die Funktion zeigt.

Da Funktionen also in Lua ganz normale Werte sind, ist es möglich, Funktionen zweiter Ordnung zu schreiben. Das sind Funktionen, die ihrerseits Funktionen als Argumente verarbeiten oder Funktionen zurückliefern. Das ist in vielen Situationen sehr nützlich, aber ein Thema für fortgeschrittene Lua-Programmiererinnen.

Navigation

Übersicht

Vorherige Lektion

Nächste Lektion


Wir bedanken uns bei der Peakboard GmbH für die freundliche Unterstützung bei der Entwicklung dieses Kurses.

Peakboard ist eine All-in-One-Lösung aus Soft- und Hardware, mit der Du Daten aus unterschiedlichen Datenquellen erhebst, auswertest und in Echtzeit auf Bildschirmen visualisierst. Mit der kostenlosen Software, dem Peakboard Designer, gestaltest Du Dein individuelles Dashboard und bindest deine Datenschnittstellen an. Die Hardware, die Peakboard Box, verarbeitet und kommuniziert die Datenströme dezentral und damit ressourcenschonend direkt am Industriearbeitsplatz. Damit sorgst Du für mehr Transparenz und optimierst so ganz einfach deine Prozesse.