Hilfe:Lua/Modul im Wiki

aus Wikipedia, der freien Enzyklopädie

{{Wikipedia:Lua/Linkbox}} Diese Hilfeseite stellt für Lua-Programmierer die Wirkung eines Lua-Moduls im Kontext von Wiki-Seiten dar.

Quelltext und Seiten

  • Alle Seiten im Namensraum Modul: (828) sind grundsätzlich der Quellcode zu je einem Lua-Modul. Dies wird nicht als Wikitext interpretiert. Das Content Model ist Scribunto.
  • Nur Seiten, deren Name das Schema für Dokumentationen erfüllt, in der deutschsprachigen Wikipedia also auf /Doku endet, können Wikitext enthalten.
  • Darüber hinaus gibt es die Möglichkeit, in bestimmten Unterseiten der Benutzer-Spielwiese im Rahmen der Vorlagenspielwiese Lua-Module wirksam werden zu lassen.
  • Seiten anderer Namensräume können mit #invoke schon deshalb nicht genutzt werden, weil der Namensraum dieser Anweisung intern vorangestellt wird. Auch ein require() ist nur auf Seiten möglich, deren Inhalt als Lua-Quellcode gespeichert ist („Content Model“).
  • Sonstige Unterseiten im Namensraum Modul: sollten nur nicht-selbstständigen Lua-Code enthalten:
    • Keine Vorlage oder ein anderes Modul sollte unaufgefordert derartige Seiten verwenden; ausgenommen durch mw.loadData() genutzte Datenmengen oder wenn eine solche Schnittstelle dokumentiert ist.
    • In der Regel ist es aus dem selbstständigen Modul ausgegliederter Code, wofür es unterschiedliche gute Gründe geben kann.
    • Es könnte auch ein unter dem Namen /test ausgegliederter unit test sein.

Kapselung

Die Module sind bewusst so gestaltet, dass keinerlei Zugriff von einer Einbindung mit #invoke auf eine andere oder den Inhalt der umgebenden Seite möglich ist. Einige trickreiche Versuche, dies zu unterlaufen, wurden nach kurzer Zeit unterbunden.[1]

Ein Modul kann selbst keinerlei Veränderung an der Welt hervorrufen (sieht man davon ab, dass bei den „Links auf diese Seite“ durch Einbindung anderer Seiten ein Eintrag hinzukommen kann; oder dass es die Kategorie der Seiten mit Skriptfehlern bereichert). Es kann sich ausschließlich durch seinen Rückgabewert äußern, der letztlich in irgendeiner Weise auf eine Vorlagen-Einbindung oder sonstigen Seitentext wirken müsste.

Das Modul kann die Parameter der Vorlagen-Einbindung (oder einer sonstigen Seite, in der das #invoke vorkommt) auslesen. Ihm ist außerdem bekannt, wie die Gesamt-Seite heißt, die dem Benutzer dargestellt wird. Mehr kann das Modul über seine Umgebung (Elternumgebung) nicht herausfinden. Wie die Vorlage heißt, in der #invoke steht, lässt sich mit frame:getParent():getTitle() auch noch ermitteln (siehe unten). seinen eigenen Namen kann das Modul seit Anfang 2014 ermitteln.

Die globale Variable _G

_G ist eine table, die standardmäßig die nachstehend aufgelisteten Komponenten enthält:

_G _VERSION assert debug error getmetatable ipairs math mw next os package pairs pcall rawequal rawget rawset require select setmetatable string table tonumber tostring trim type unpack xpcall

Im Handbuch auf Mediawiki heißt es:

Note that every required module is loaded in its own sandboxed environment, so it cannot export global variables as is sometimes done in Lua 5.1. Instead, everything that the module wishes to export should be included in the table returned by the module.

Tatsächlich sind jedoch im Mai 2013 in _G aus allen mittels require() eingebundenen Modulen diejenigen Variablen versammelt, die dort nicht explizit als local deklariert wurden. Es ist aber davon auszugehen, dass dies nicht mehr lange der Fall sein wird; besser ist gemäß Export-table eine saubere Rückgabe.

Rückgabewert des Moduls

Die letzte Zeile eines Moduls muss eine return-Anweisung mit einem Rückgabewert sein. Fehlt dies, wäre der Wert immer nil.

Der Datentyp dieses Rückgabewerts ist im Prinzip beliebig. Verbreitet wäre table, aber auch eine function wäre vorstellbar; letztlich auch eine zwangsläufig kaum variierbare Zeichenkette oder Zahl.

Der Rückgabewert ist in drei Fällen auszuwerten:

  1. Als Wert des Aufrufs von require().
  2. Als Wert von mw.loadData().
  3. Wenn es eine table ist, macht sie für #invoke die verfügbaren Funktionen zugreifbar.

Schnittstelle zur Vorlagenprogrammierung

In die einbindende Seite wird mittels der Parserfunktion {{#invoke:M|fun|params}} eingebettet.

fun ist der Name einer Funktion, die Komponente in dem als table deklarierten Rückgabewert des Moduls M ist. Sie erhält als Parameter das Objekt frame des #invoke.

Die Parameterliste entspricht derjenigen bei Vorlagen. Verirrte Gleichheitszeichen im Wert eines unbenannten Parameters können Syntaxfehler auslösen. Bei von außen kommenden Werten sollte immer mittels vorangestellter laufender Nummer eine sichere Zuweisung erfolgen: |1={{{Text|}}}

Auswertung in der Funktion

In der Funktion steht die Parameterliste im Objekt frame als table frame.args zur Verfügung.

Im Unterschied zu Vorlagen ist auch die leere Zuweisung |x= | auswertbar; das heißt: Es lässt sich ohne besondere Konstrukte feststellen, dass x in der Parameterliste genannt wird, jedoch der leere Wert zugewiesen wurde.

Der Wert einer Komponente von frame.args ist ein string, wenn der Parameter im #invoke vorkommt, und nil ansonsten.

Wie auch bei Vorlagen gilt: Benannte Parameter werden getrimmt (Leerzeichen und Zeilenumbruch vor und nach dem Wert werden entfernt); unbenannte Parameter nicht.

Als Beispiel siehe das Modul:Hello.


Die Zuordnung lässt sich am besten aus folgendem Beispiel für ein Modul M mit Funktion f ablesen:

{{#invoke:M|f|1=ABC|FGH| LMN |benanntA=PQR|benanntB= TUV | HÖHE ÜBER NN= 32 | Leergut= }}

liefert

frame.args[ 1 ] »ABC«
frame.args[ 2 ] »FGH«
frame.args[ 3 ] » LMN «
frame.args.benanntA »PQR«
frame.args.benanntB »TUV«
frame.args[ "HÖHE ÜBER NN" ] »32«
frame.args.Leergut »«
frame.args.Unbekannt nil

Rückgabewert einer Funktion für Vorlagen

Der Rückgabewert der in #invoke genannten Funktion ersetzt als Zeichenkette den Bereich {{#invoke:}}.

Als Rückgabewert sollte man immer explizit eine Zeichenkette schaffen. Wenn der Rückgabewert von anderem Typ ist, wird dies durch Standard-Operationen in eine Zeichenkette umgewandelt. Dadurch kann in der Seite das Wort false oder nil lesbar sein oder die Programmierung zu falschen Schlüssen führen. Letzteres lässt sich vermeiden durch den Ausdruck:

return r or ""

Wenn r „etwas“ ist, wird r zurückgegeben, sonst "".

Mehrere Rückgabewerte oder ein aggregierter Datentyp würden für #invoke automatisch durch Verwendung der Funktion tostring() in Strings umgewandelt und dann ohne Trennzeichen aneinandergefügt. Dies sollte vermieden werden und kontrolliert eine Zeichenkette gebildet werden, die sich dann auch im Artikel sehen lassen kann.

Objekt frame

Scribunto-Dokumentation (englisch)

Das Objekt frame gibt die Umgebung wieder. Grundsätzlich gibt es zwei Instanzen:

  1. Aufruf des #invoke (Normalfall; standardmäßig immer mit frame bezeichnet)
  2. Aufruf der einbindenden Seite (meist Vorlage).
    • Erhältlich mittels frame:getParent()
    • Einziger nutzbarer Unterschied sind die Argumente frame.args der Vorlageneinbindung; alle Funktionen führen zum gleichen Ergebnis; erneutes frame:getParent() hilft nicht.

Wenn über #invoke eine Funktion für die Vorlage genutzt wird, ist der Funktion gemäß Schnittstelle zur Vorlagenprogrammierung auch das Objekt frame dazu bekannt. Falls dies (als Bibliotheksfunktion) nicht übergeben wurde oder in tieferen Ebenen nicht durchgereicht wurde, kann es überall gebildet werden mit:

frame = mw.getCurrentFrame()

Komponenten

Unmittelbar nutzbar sind:

frame.args
Angegebene Parameter als table.
  • In der Regel Parameter des #invoke – nach frame:getParent() Vorlagenparameter der einbindenden Seite.
Aus Performance-Gründen wird das frame-Objekt als metatable organisiert. Das bedeutet, dass insbesondere die frame.args nicht manipuliert werden können und auch die Anzahl nicht per length-Operator # festgestellt werden kann.
Der Datentyp des Bezeichners ist:
  • number bei einem unbenannnten Vorlagenparameter; bzw. numerischer Angabe
  • string sonst
frame:callParserFunction()
Ausführung einer Parser-Funktion.
r = frame:callParserFunction( name, args )
Verschiedene Formen sind für die Argumentenliste möglich.
Die Argumente werden unverarbeitet an die Parser-Funktion übergeben.
Der Doppelpunkt gehört nicht zum Funktionsnamen.
Wo es möglich ist, sollten Lua-interne Funktionen bevorzugt werden.
frame:expandTemplate()
Einbinden einer anderen Seite.
r = frame:expandTemplate{ title = s, args = args }
Es ist nur dieses Format mit benannten Parametern und geschweiften Klammern möglich.
title= wird in der Regel eine Zeichenkette mit dem Vorlagentitel sein; inzwischen ist aber auch ein Objekt mw.title möglich.
frame:extensionTag()
Analog zu frame:callParserFunction() mit dem von Tags umschlossenen Inhalt.
r = frame:extensionTag( name, content, args )
Verschiedene Formen sind für die Argumentenliste möglich.
frame:getParent()
Objekt frame für die einbindende Seite (Elternumgebung).
frameT = frame:getParent()
frame:getTitle()
Gibt den Seitentitel zum frame-Objekt zurück.
Für den mittels #invoke generierten frame ist das der Name des eingebundenen Moduls selbst (einschließlich Modul: zu Beginn).
Das Modul kennt dadurch seinen eigenen Namen (Pfad).
Das kann eingesetzt werden, um die Produktivversion von einer Testkopie im BNR zu unterscheiden.
Maximal die einbindende Wikitext-Seite, in der Regel also die Vorlage mit dem #invoke, lässt sich so ermitteln: frame:getParent():getTitle()
Von der einbindenden Wikitext-Seite (Vorlage) aus ist keine Steigerung mehr im Sinne eines Aufruf-Stack möglich; die übergeordneten Aufrufe liefern nil zurück.
frame:preprocess()
Expandieren eines komplexen Wikitexts.
Vorlagen, Parserfunktionen und Parameter wie {{{1}}} werden ausgewertet.
Eine einzelne Vorlage sollte statt dessen mit frame:expandTemplate() expandiert werden.
frame:preprocess( string )

Nicht zwingend benötigt werden die nachstehenden drei Funktionen. Sie liefern jeweils ein object (table) zurück. Das object hat eine einzige Methode object:expand() – dies liefert den expandierten Wikitext in der jeweiligen Situation.

frame:getArgument( argument )
frame:getArgument{ name = argument }
Lässt argument expandieren.
frame:newParserValue( text )
frame:newParserValue{ text = text }
Zugriff auf frame:preprocess( text ).
frame:newTemplateParserValue{ title = title, args = table }
Benannte Argumente erforderlich.
Zugriff auf frame:expandTemplate() mit den genannten Argumenten.

Substituierung

Mittels der Funktion mw.isSubsting() lässt sich herausfinden (boolean), ob die momentane Aktivität darauf hinauslaufen wird, dass das Ergebnis substituiert wird (mit {{subst: eingebunden wurde).

Anfang 2014 wird jedoch nur erkannt, ob die Parserfunktion #invoke unmittelbar Gegenstand einer Substituierung {{subst:#invoke:m|f}} ist; nicht aber, ob etwa eine Vorlage, in der dieses #invoke vorkäme, gerade substituiert wird.

Dadurch lassen sich Wartungskategorien in das Ergebnis einfügen, die bei einer unerwünschten Substituierung protestieren; oder umgekehrt bei fehlender Substituierung eine Einbindung bemängeln.

Ein frame-Objekt wird bei der Substituierung von #invoke nicht übergeben; über mw.getCurrentFrame() lässt es sich aber jederzeit beschaffen.

Mehrere Module

Einbindung

Die Funktion require() bindet ein anderes Modul ein.

  • Parameter ist eine Zeichenkette mit dem Namen des Moduls.
    • Der Namensraum Modul: ist voranzustellen.
  • Es ergeben sich folgende Werte:
    1. table oder string, wenn dies Rückgabewert war.
    2. true wenn der Seitenname sonst existiert und als Lua-Quellcode verwaltet wird.
    3. Skriptfehler und Abbruch, wenn der Seitenname nicht existiert oder der Inhalt nicht als Lua-Quellcode gilt oder ein globaler Syntaxfehler in der Seite vorliegt (kein gültiger content).

Beispiel für eine sichere Einbindung:

local lucky, WillHaben = pcall( require, "Modul:WillHaben" )
if type( WillHaben ) == "table" then
    r = WillHaben.WillHaben()
else
    r = "<span class=\"error\">" .. WillHaben .. "</span>"
    --  auto: "package.lua:80: module 'Modul:WillHaben' not found"
    --  "Fehler * Modul:WillHaben nicht gefunden"
end

pcall() ist absturzsicher; der zweite Rückgabewert ist im Erfolgsfall der Rückgabewert des Moduls, sonst die Fehlermeldung von require().

Hinweis: Die Parserfunktion #iferror: greift nicht, wenn der Klassenname in Apostroph ' statt " eingeschlossen ist.

Weiterleitung

Ein Modul kann aus einer einzigen Zeile bestehen

return require( "Modul:NeuerName" )

und wirkt dann wie eine Weiterleitung; etwa nach einer Umbenennung.

mw.loadData()

Diese Funktion entspricht grundsätzlich dem require() mit zwei Besonderheiten:

  • Die Elemente des Rückgabewerts können nur Basis-Datentypen enthalten (vor allem Zeichenketten und Zahlen); nicht aber Funktionen.
  • Die erfolgte Einbindung wird vorgemerkt, so dass mehrfache #invoke innerhalb einer Gesamt-Seite beschleunigt werden.

Vorgesehen ist dies für große Datenmengen, die dann auch separat von den Funktionen im Modul gepflegt werden können.

Export für andere Module

Die Standard-Aufgabe ist es, eine Vorlage mittels #invoke zu unterstützen. Konzeptionell gibt es die Möglichkeit, zusätzlich oder ausschließlich andere Lua-Module durch require() zu unterstützen. Dabei gibt es zwei Fälle:

  1. Sowohl Vorlagen als auch Module werden durch jeweils angepasste Funktionen gleicher Zielsetzung unterstützt.
    • Es bietet sich an, die Funktionen mit gleichem Namen in beiden Zugriffsarten anzubieten.
      • Die generische Funktion ist dann die Version für Lua-Aufruf.
      • Die Variante für die Vorlagenprogrammierung setzt nur noch die Eingabeparameter um, ruft die generische Funktion auf und stellt das für Vorlagen geeignete Resultat sicher.
      • Bei der Vorlagenprogrammierung sind ausschließlich Zeichenketten als Parameter möglich, während die generische Funktion auch andere Typen wie numerische Werte oder table zulässt. Die beim #invoke beigegebenen Parameterwerte sind dann entsprechend zu interpretieren (parsen).
    • Beispiel: siehe unten
  2. Es werden ausschließlich Funktionen für Lua-Module angeboten; die Aufgabenstellung passt nicht zu Vorlagen.

Exportierte Funktionen werden mitsamt der table ihres definierenden Moduls angewendet. Diese heißt immer so wie das Modul selbst. Da die Namen der Module im Namensraum eindeutig sind, kann es nicht zu Namenskonflikten kommen. Damit ist auch sofort klar, woher eine Funktion stammt und wo die Dokumentation zu finden ist, wenn plötzlich im Quelltext ein unbekannter Funktionsaufruf angetroffen wird.

Einfaches Modul für Vorlagen

Das Modul:Hello gibt einen ersten Einstieg:

local p = {} 
function p.hello(frame)
    local name = frame.args[1]
    if not name then 
        name = 'Welt' 
    end
    return  'Hallo, ' .. name .. '! Dies ist Lua!' 
end 
return p

Der Aufruf würde erfolgen mittels

  • {{#invoke:Hello|hello}} (ergibt Hallo, Welt! Dies ist Lua!)

oder

  • {{#invoke:Hello|hello|hier bin ich}} (ergibt Hallo, hier bin ich! Dies ist Lua!)

Strukturierung eines komplexeren Moduls

Ein Modul mit zwei oder drei Funktionen nur für Vorlagen ist eher trivial.

Bei komplexeren Aufgaben ist etwas Strukturierung und Übersicht erforderlich.

  • An Beginn sollte auch ein Datum vermerkt sein.
    • Weil Versionen des Quellcodes nicht ausschließlich über die produktive Seite generiert werden, ist die normale Versionsgeschichte einer Seite nicht hinreichend für die Verfolgung der Varianten. Vielmehr werden Versionen auch aus einem Testwiki oder von einer gesonderten Erprobungsseite oder aus einem fremden Wiki-Projekt mit der Mutterversion einkopiert; und wieder dorthin zurück.
  • Es empfiehlt sich eine Ultrakurzbeschreibung der Funktionalität.
  • Am Schluss stehen die Funktionen, die für den Export bestimmt sind.
  • p ist der Rückgabewert des gesamten Moduls. Dieser Name hat sich eingebürgert, und er stellt eine table dar.

Funktionen nur für Vorlagen, sowie innere Hilfsfunktionen

--[=[ DiesesBeispiel 2013-05-07
Dieser und jener Zweck
* service
]=]

-- Module globals
local messagePrefix = "lua-module-DiesesBeispiel-"
-- ...
-- ...

local function x( a )
    -- ...
    return r
end -- x()

-- ...
-- ...


-- Provide template access
local p = {}

p.service( frame )
    -- ...
    return x( u ) or ""
end -- .service()

return p

Die Funktion service wird zur Nutzung in einer Vorlage exportiert.

Funktionen für Vorlagen und zusätzlich Lua-Zugriff

--[=[ DiesesBeispiel 2013-05-07
Dieser und jener Zweck
* service
* DiesesBeispiel()
]=]

-- Module globals
local messagePrefix = "lua-module-DiesesBeispiel-"
local DiesesBeispiel = { }
-- ...
-- ...

local function x( a )
    -- ...
    return r
end -- x()


DiesesBeispiel.service = function ( a )
    -- ...
    return x( u )
end -- DiesesBeispiel.service()


-- ...
-- ...

-- Provide external access
local p = {}

p.service( frame )
    -- ...
    return DiesesBeispiel.service( u ) or ""
end -- .service()

function p.DiesesBeispiel()
    -- Retrieve function access for modules
    -- Postcondition:
    --     Return table with functions
    return DiesesBeispiel
end -- .DiesesBeispiel()

return p

Wie vor, aber es wird zusätzlich die table DiesesBeispiel exportiert über die Funktion DiesesBeispiel().

  • Die Funktion DiesesBeispiel.service() steht danach auch anderen Modulen zur Verfügung.
  • Die eigentliche Implementierung der Funktionalität erfolgt in DiesesBeispiel.service() und die Funktion service für Vorlagen ruft diese Implementierung auf. Dabei sind die Parameter der Vorlageneinbindung (Zeichenketten in frame.args) geeignet zu interpretieren und ihr Typ ggf. geeignet zu konvertieren. Während der Rückgabewert von DiesesBeispiel.service() auch false oder nil sein mag, wird durch or "" die für Vorlagen geeignetere leere Zeichenkette zurückgegeben.

Funktionen ausschließlich für Lua-Zugriff, nicht für Vorlagen

--[=[ DiesesBeispiel 2013-05-07
Dieser und jener Zweck
* DiesesBeispiel.service()
]=]

-- Module globals
local messagePrefix = "lua-module-DiesesBeispiel-"
local DiesesBeispiel = { }
-- ...
-- ...

local function x( a )
    -- ...
    return r
end -- x()


DiesesBeispiel.service = function ( a )
    -- ...
    return x( u )
end -- DiesesBeispiel.service()


-- ...
-- ...

-- Provide external access

return DiesesBeispiel

Der Export von p mit der Tabelle für Vorlagen entfällt; es wird nur die table DiesesBeispiel direkt exportiert.

Diese Situation tritt beispielsweise bei ausgelagerten Unter-Modulen sowie Datentabellen bei mw.loadData() auf.

Elternumgebung

Versuche, die programmtechnisch im Prinzip vorhandene Elternumgebung zu ergründen, werden von Scribunto mit dem Ziel der Kapselung systematisch unterbunden. Darin unterscheidet sich „Lua im Wiki“ von allgemeinen Lua-Anwendungen.

Der einzig sinnvoll mögliche Zugriff ist auf die Parameter der unmittelbar einbindenden Seite möglich:
frame:getParent().args

Innerhalb der unmittelbar umgebenden Seite (Vorlage) erfolgen die einzelnen Aufrufe von #invoke sequentiell und unter Verwendung der gleichen Umgebung.

  • Verschiedene Einbindungen der Vorlage selbst (z. B. in einem Artikel) werden dagegen parallel abgearbeitet und teilen auch keine Elternumgebung.

math.random()

Vor dem ersten Aufruf der Funktion math.random() ist der Zufallszahlengenerator stets mit dem Wert 0 initialisiert (zur Initialisierung siehe math.randomseed()). Ohne eigene Initialisierung wird also stets die gleiche Folge von Zufallszahlen generiert. Bei sequentieller Verarbeitung der Aufrufe von #invoke innerhalb der gleichen Vorlage wird der Zufallszahlengenerator allerdings zwischendurch nicht zurückgesetzt, so dass die Folge dort kontinuierlich fortgesetzt wird.

Anmerkungen