Fay

Haskell auf Client-Seite

Alexander Mills (inf9433@fh-wedel.de)

Startseite | Inhaltsverzeichnis | Einleitung | Funktionsweise | Beispiele | Einschränkungen | Fazit | Quellen

Funktionsweise

Dieses Kapitel beschäftigt sich mit der Art und Weise, mit der Fay Haskell-Programme zu JavaScript kompiliert. Hierzu gehört der grundlegende Aufbau der generierten Datei sowie die Art und Weise der Umwandlung des Haskellcodes zu JavaScriptcode.

Grundlegender Aufbau

Die erzeugte JavaScriptdatei setzt sich aus folgenden Komponenten zusammen:

In der generierten JavaScriptdatei haben alle Funktionen innerhalb der Main-Klasse eine bestimmte Namenskonvention. Die Namen beginnen mit einem Präfix für das jeweilige Modul, gefolgt von einem Dollarzeichen als Trennsymbol und zuletzt den eigentlichen Funktionsnamen. Die Fay-Runtimefunktionen besitzen zwei Dollarzeichen, um einen Konflikt mit dem Namen für den Haskellcode zu vermeiden. Somit ist es nicht möglich mit einem Haskellmodul namens "Fay" die Laufzeitfunktionen zu überschreiben.

    var Main = function() {
    /**Fay-Runtime*/
    Fay$$...
    ...
    /**Prelude*/
    Prelude$...
    ...
    /**Der gewünschte Code*/
    Main$...
    ...
    }
    var main = new Main()
    /** run main */
  

Fay Runtime

Bei der Kompilierung von Haskell nach JavaScript fügt der Compiler die Laufzeitfunktionen von Fay der generierten Datei hinzu. Diese bestehen hauptsächlich aus:

Die Fay-Runtime liegt als JavaScript-Code vor und wird beim Kompilieren in den erzeugten Code kopiert.

Thunks und Lazy Evaluation

Um die Lazy Evaluation in JavaScript zu implementieren, setzt Fay die Thunks aus Haskell um.

Thunks sind noch nicht berechnete Werte. Sie werden erst dann berechnet, wenn der eigentliche Wert benötigt wird. So ist es in Haskell beispielsweise möglich eine Funktion auf eine unendlich lange Liste anzuwenden, wenn diese Funktion nicht die gesamte Liste verwertet, da nur der erste Wert der Liste berechnet wird.


Die Implementierung von Thunks ist relativ schlicht.

    function Fay$$$(value){
      this.forced = false;
      this.value = value;
    }
  

Im Feld "value" wird ein Wert oder ein Funktionszeiger gespeichert. Das Feld "forced" gibt an, ob der Wert schon berechnet wurde ("True") oder ob die Funktion in "value" noch ausgeführt werden muss ("False").

Der Konstruktor für Thunks wird im JavaScriptcode für jede Funktion verwendet, die nicht-strikt auswerten soll. Hierzu gehören generell alle übersetzten Funktionen, ausgenommen konstante Funktionen die keine Berechnungen vornehmen.

Da der Konstruktor sehr häufig vorkommt, wurde um den Code übersichtlicher zu machen ein kurzer Name gewählt, welcher sich nur aus dem Präfix "Fay$$" und dem Funktionsnamen "$" zusammensetzt.


Die Auswertung eines Thunks erfolgt über die Funktion "force", welche ein Objekt als Parameter bekommt und dessen "value"-Feld zurückgibt, wenn es sich dabei um ein Thunk handelt.

"force" unterscheidet dabei zwischen mehreren Fällen:

Da die Funktion "force" noch häufiger im generierten Code benutzt wird als der Konstruktor für Thunks, wurde auch hier ein kurzer Name vergeben. Somit heißt die Funktion einfach "Fay$$_".


Funktionen

Die Funktionen aus dem Haskellcode werden mithilfe von Thunks übersetzt. Funktionsaufrufe wiederum werden mit "force" realisiert. Der übersetzte Code einer Funktion wird, in einer Funktion gekapselt, als Parameter für den Thunkkonstruktor verwendet.

Dieser Thunk wird dann in einer Variablen gespeichert, deren Name sich aus der Namenskonvention ergibt. Eine Funktion "f" in einem nicht benannten Modul heißt beispielsweise "Main$f".

    var Main$f = Fay$$$(function(){...})
  

Hat die Funktion einen oder mehrere Parameter, enthält die Variable nicht direkt den Thunk, sondern eine Verschachtelung von Funktionen, deren Anzahl gleich der Anzahl der Parameter ist und auf deren untersten Schicht der Thunk liegt.

Beispiel für eine Funktion "f" mit 2 Parametern

    var Main$f = function($p1) {
              function($p2) {
                Fay$$$(function () {...})
              };
            };
  

Die Parameter "$p1" und "$p2" werden innerhalb des Rumpfes der Funktion im Thunk verwendet. Diese Art der Parameterübergabe ist außerdem die Umsetung von "Currying", was bedeutet, dass nicht alle Parameter zeitgleich angegeben werden müssen.

Wird die Funktion "Main$f" aufgerufen, so wird für jeden Parameter einmal "force" angewendet und anschließend ein Parameter übergeben.

    //Haskellcode:
    f 1 2
    =>
    //JavaScriptCode
    Fay$$_(Fay$$_(Main$f)(1))(2))
  

Der Rückgabewert des Aufrufs ist der Thunk, welcher innerhalb der zwei verschachtelten Funktionen liegt. Ein weiterer Aufruf von "force" würde die Berechnung der Funktion starten. Dies geschieht jedoch erst, wenn dieser Wert benötigt wird.


Theoretisch könnte auch einfach folgender Aufruf geschrieben werden:

    (Main$f(1))(2)
  

Die "force"-Funktion gibt jedes mal nur die Funktion zurück, da es sich nicht um ein Thunk handelt. Jedoch lässt sich aus der Signatur einer Funktion, also der Anzahl und Art der Parameter, nicht schließen, was für einen Wert die Funktion zurückgibt. Dies lässt sich an folgendem Beispiel zeigen:

    add :: Int -> Int -> Int
    add a b = a + b
    
    add' :: Int -> Int -> Int
    add' = \ x -> \ y -> x + y
  

Diese beiden Funktionen sind gleichwertig, jedoch ist der Thunkkonstruktor für die untere Implementierung nicht in zwei Funktionen verschachtelt. "Main$add'(1)" würde also einen Fehler zurückgeben, da es sich nicht um eine Funktion handelt. Deshalb wird prinzipiell immer je Parameter einmal "force" angewendet, auch wenn es nicht immer nötig ist.


Ein weiterer relevanter Bestandteil von Funktionen ist das Patternmatching. Dieses lässt sich jedoch relativ leicht nach JavaScript übersetzen, wenn man sich seiner Bedeutung bewusst ist.

    f :: Int -> Int
    f 0 = 0
    f n = ...
  

Umgangsprachlich kann die zweite Zeile wie folgt formuliert werden: "Wenn der erste Parameter Null ist, gib Null zurück". Entsprechend übersetzt Fay das Patternmatching durch eine If-Abfrage.

    if (Fay$$_($p1) === 0) {
      return 0;
    }
  

Bevor der Parameter verglichen werden kann, wird er zunächst ausgewertet, da die Möglichkeit besteht, dass es sich hierbei noch um einen Thunk handelt.


Patternmatching ist in Haskell auch mit Typkonstruktoren möglich, wenn beispielsweise ein Summendatentyp als Parameter übergeben wird. Für diese erzeugt Fay beim Kompilieren einen Klassenkonstruktor, welcher einen Wert kapselt. Verglichen wird beim Patternmatching dann der Klassentyp mit der JavaScriptfunktion "instanceof".

    data Value = Text String
            | Num Int
    
    isNum :: Value -> Boolean
    isNum (Text s) = False
    isNum (Number i) = True
  

Im kompilierten Code:

    ...
    function $_Main$Text(slot1){
      this.slot1 = slot1;
    };
    function $_Main$Number(slot1){
      this.slot1 = slot1;
    };
    ...
    if (Fay$$_($p1) instanceof $_Main$Text) {
      var s = Fay$$_($p1).slot1;
      return false;
    }
    if (Fay$$_($p1) instanceof $_Main$Number) {
      var i = Fay$$_($p1).slot1;
      return true;
    }
  

Ein Beispiel

Nach dem erläuterten Übersetzungsschema lassen sich nun Funktionen übersetzen. Als Beispiel dient die naive Implementierung der Fibonacci-Funktion

    fib :: Int -> Int
    fib 0 = 0
    fib 1 = 1
    fib n = fib (n - 1) + fib (n - 2)
  

Aus dem Namen der Funktion und dem Modulnamen wird ein Variablenname erzeugt.

    var Main$fib = ...
  

Da kein expliziter Modulname angegeben wurde, wird der Name "Main" automatisch angenommen.


In dieser Variablen wird eine Funktion gespeichert. Für jeden Eingabeparameter wird eine neue Verschachtelungsebene generiert. Die Funktion "fib" besitzt nur einen Parameter, also nur eine Verschachtelung.

    var Main$fib = function($p1){
    ...
    };
  

Diese Funktion gibt nun einen Thunk zurück, dessen "value"-Feld die eigentliche Funktion ist.

    var Main$fib = function($p1){
      return new Fay$$$(function(){
        ...
      });
    };
  

Als nächstes folgt das Patternmatching.

    var Main$fib = function($p1){
      return new Fay$$$(function(){
        if (Fay$$_($p1) === 0) {
          return 0;
        }
        if (Fay$$_($p1) === 1) {
          return 1;
        }
        ...
      });
    };
  

Der letzte Abschnitt ist der rekursive Aufruf, welcher aus fünf Funktionsaufrufen (zwei Subtraktionen, eine Addition und zwei rekursive Abstiege) besteht. Der Parameter wird in einer lokalen Variablen gespeichert, deren deren Name sich aus der Haskellfunktion ergibt.

    var Main$fib = function($p1){
      return new Fay$$$(function(){
        if (Fay$$_($p1) === 0) {
          return 0;
        }
        if (Fay$$_($p1) === 1) {
          return 1;
        }
        var n = $p1;
        return Fay$$_(Fay$$_(Fay$$add)(
          Fay$$_(Main$fib)
            (Fay$$_(Fay$$_(Fay$$sub)(n))(1))))
          (Fay$$_(Main$fib)
            (Fay$$_(Fay$$_(Fay$$sub)(n))(2)));
      });
    };
  

Der letzte Teil wirkt auf den ersten Blick unübersichtlich. Wäre hier ein längerer Name als "Fay$$_" für die "force"-Funktion gewählt worden, wäre der Code noch schwieriger zu lesen. Ist der Code so formatiert wie hier, lassen sich jedoch leicht die einzelnen Bestandteile erkennen.


Datentypen

JavaScript hat eine dynamische Typisierung und es gibt keine expliziten Typangaben. Korrekte Typverwendung innerhalb des Haskellcodes wird während des Kompiliervorgangs geprüft, während der JavaScriptcode typfrei bleibt. Die einzige Möglichkeit, die Typkonsistenz während im JavaScriptcode zu sichern, wären Typüberprüfungen zur Laufzeit, was wiederum zu einer langsameren Ausführung des Codes führt.

Einzige Ausnahme sind die schon erwähnten Typkonstruktoren der Summendatentypen, da hier für das Patternmatching ein Vergleich der Typen stattfinden muss.


Listen

Bei der Kompilierung von Listen aus Haskell gilt es für Fay zu beachten, dass diese teilweise "lazy" ausgewertet werden müssen. Um zu zeigen wie Fay mit Listen umgeht, dienen folgende drei Listen als Beispiel:

    [1,2,3]
    [1..1000]
    [1..]
  

Die erste Liste ist komplett definiert und hat drei Elemente. Von der zweiten Liste ist zwar für 1000 Elemente definiert, jedoch sind wird sie nicht sofort komplett berechnet und in den Arbeitsspeicher gelegt, sondern erst bei der Auswertung der einzelnen Elemente. Dies geschieht beispielsweise wenn man alle Elemente dieser Liste ausgeben möchte.

Die dritte Liste hat unendlich viele Elemente, nämlich alle natürlichen Zahlen größer als Null, und der Versuch sie ausgeben zu lassen würde unendlich lange dauern.


Für Listen hat Fay mehrere Konstruktoren in der Laufzeit implementiert. Die erste Liste wird wie folgt übersetzt:

    Fay$$list([1,2,3])
  

Der Parameter der hier übergeben wird ist ein Array. "Fay$$list" verpackt alle Elemente dieser Liste in der Listenstruktur welche ebenfalls in der Laufzeit implementiert ist. Die Listenstruktur ist die klassische lineare Liste, in welcher jedes Element ein Wert und einen Nachfolger hat.

Der Konstruktor für ein Listenelement ist der folgende:

    function Fay$$Cons(car,cdr){
      this.car = car;
      this.cdr = cdr;
    }
  

"Fay$$list" macht aus dem Array "1,2,3" dann folgendes Konstrukt:

    Cons(1,Cons(2,Cons(3,null)))
  

Die Liste ist bei der Ausführung komplett berechnet. Die Umsetzung der unendlichen Liste ist dabei etwas komplexer. Anstelle von "Fay$$list" wird die Funktion "Prelude$enumFrom" genutzt, welche auch auf den Konstruktor für die Liste zugreift, jedoch nicht die gesamte Liste generiert.

Die Funktion gibt selber, genau wie die meisten anderen übersetzten Funktionen, einen Thunk zurück, welcher in einer Funktion für die Parameterannahme verschachtelt ist. Bei der Berechnung gibt die Funktion ein neues "Cons" zurück, deren Kopfelement der Parameter ist, welcher "enumFrom" übergeben wurde. Die Restliste ist ein rekursiver Aufruf, wobei der alter Parameter um eins inkementiert wird und als Parameter für die rekursive Funktion dient.

Der folgende Pseudocode zeigt wie "[1..]" übersetzt wird:

    enumFrom(1) = Cons(1, enumFrom(2))
  

Operationen, welche nicht die gesamte Liste verarbeiten, lassen sich auf diese unendliche Liste anwenden. "head" wird das "car"-Element aus dem ersten "Cons" zurückgeben, ohne die Restliste überhaupt zu berechnen. Eine Funktion wie "take", welche die ersten n Elemente einer Liste zurückgibt, wird auch nur die ersten n Elemente dieser unendlichen Liste berechnen.


Die letzte Funktion, welche für "[1..1000]" verwendet wird, heißt "Prelude$enumFromTo" und funktioniert ähnlich wie "Prelude$enumFrom", mit dem unterschied, dass sie zwei Parameter besitzt und abbricht, sobald der erste Parameter größer ist als der Zweite.

    enumFromTo(1,1000) = Cons(1, enumFromTo(2,1000))
  

Foreign Function Interface

Auch wenn es möglich ist den Haskellcode nach JavaScript zu kompilieren, so fehlt Haskell die Möglichkeit auf die DOM-Struktur einer HTML-Seite zuzugreifen. Um dies zu ermöglichen stellt Fay das "Foreign Function Interface", kurz "FFI" zur Verfügung. Durch Einbinden des FFI-Moduls ist es möglich JavaScriptcode innerhalb des Haskellcodes zu nutzen.

Dieses Beispiel zeigt ein "Hello World"-Programm, welches die "alert"-Funktion von JavaScript nutzt:

    module Hello (main) where
    
    import FFI

    main = alert "hello world"

    alert :: String -> Fay ()
    alert = ffi "alert(%1)"
  

Die Funktion FFI erwartet als Parameter einen String, der eine JavaScriptfunktion enthält. Über "%1", "%2", etc. lassen sich die Parameter der Haskellfunktion als Parameter der JavaScriptfunktion verwenden. "%1" ist in dem Beispielcode der erste Parameter der Funktion "alert".

Zur Verwendung der FFI-Funktion ist ausschließlich punktfreie Notation gestattet, da nicht das übliche Kompilierschema verwendet wird. Der Versuch den folgenden Code mit Fay zu kompilieren würde einen Fehler hervorrufen.

    alert s = ffi "alert(%1)" s
  

Bei der Übersetzung des "ffi"-Aufrufs nach JavaScript ist die Typsignatur der Funktion relavant, die "ffi" benutzt. Da JavaScript nicht mit der Datenstruktur der Haskellparameter arbeiten kann, da diese unter anderem Thunks sind, werden die Parameter für die im "ffi"-Block genannte Funktion in ein von JavaScript kompatibles Format gebracht.

    doubleToInt :: Double -> Int
    doubleToInt = ffi "Math.round(%1)"
  

In dieser Funktion wird ein Double als Parameter verwendet und ein Int zurückgegeben. Für die Konvertierung stellt die Fay-Laufzeit einige Funktionen bereit, die "fayToJs_...", bzw "jsToFay_..." heißen.

  var Main$doubleToInt = function($p1){
    return new Fay$$$(function(){
      return Fay$$jsToFay_int(Math.round(Fay$$fayToJs_double($p1)));
    });
  };
  

Der eigentliche JavaScriptcode wird direkt übertragen. Es findet keine Compilerprüfung dieses Codes statt. Stattdessen muss der Entwickler sicherstellen, dass die Funktion fehlerfrei ausgeführt wird, da es ansonsten zu Laufzeitfehlern kommen kann.

Die "fayToJs"-Funktionen haben unter anderem die Aufgabe den Parameter auszuwerten, da es sich um einen Thunk handeln könnte. Für die drei Datentypen Boolean, Int und Double findet abgesehen von der Auswertung mithilfe der "force"-Funktion jedoch keine Veränderung statt.

    function Fay$$jsToFay_int(x){return x;}
    function Fay$$jsToFay_double(x){return x;}
    function Fay$$jsToFay_bool(x){return x;}

    function Fay$$fayToJs_int(x){return Fay$$_(x);}
    function Fay$$fayToJs_double(x){return Fay$$_(x);}
    function Fay$$fayToJs_bool(x){return Fay$$_(x);}
  

Für andere Datentypen, wie zum Beispiel Listen und Strings, findet eine Konvertierung statt. "jsToFay_string" ruft die Funktion "Fay$$list" auf, da in Haskell Strings Listen von Charactern sind. Ist der Eingabeparameter ein String, so wird dieser von einer Liste zu einem String ausgewertet.


Hat eine JavaScriptfunktion keinen Rückgabewert, wie es beispielsweise bei "alert" der Fall ist, so kann die Fay-Monade genutzt werden um dies im Haskellcode zu signalisieren. Im übersetzten Code ist der Rückgabewert der Funktion dann ein Objekt mit einem "value"-Feld, der jedoch nicht extrahierbar ist.

  var Main$alert = function($p1){
    return new Fay$$$(function(){
      return new Fay$$Monad(Fay$$jsToFay(["unknown"],alert(Fay$$fayToJs_string($p1))));
    });
  };
  

Die allgemeine "jsToFay"-Funktion konvertiert den zweiten Eingabeparameter anhand des im ersten Parameters angegebenen Strings. Für "unknown" wird das Objekt direkt zurückgegeben. Da "alert" jedoch keinen Rückgabewert hat, gibt die Funktion "undefined" zurück, was innerhalb der Fay-Monade abgelegt wird.

An dieser Stelle ist anzumerken, dass als Folge der fehlenden Überprüfung des angegebenen JavaScriptcodes auch der Rückgabewert nicht überprüft wird. Dies macht ein Programm nicht nur fehleranfällig, es ermöglicht auch in Haskell eine Funktion mit Nebeneffekten zu programmieren.


Zugriff auf Funktionen

Alle Funktionen sind wie schon beschrieben in einem Klassenkonstruktor eingepackt und lassen sich nicht ohne Weiteres von außerhalb dieser Klasse aufrufen. Dies hängt damit zusammen, dass die übersetzen Funktionen als "private" deklariert sind, da sie in einer lokalen Variablen gespeichert sind. Um auf diese Funktionen zuzugreifen, muss zunächst der Zugriff auf sie ermöglicht werden. Hierzu dient am Ende der Klasse eine Reihe von globalen Variablendeklarationen.

Standardgemäß werden hierbei nur vier Laufzeitfunktionen ("Thunk" und "force" sowie die allgemeinen Konvertierungsfunktionen) und die Main-Funktion des Programms exportiert.

  // Exports
  this.Main$main = Main$main;

  // Built-ins
  this._ = Fay$$_;
  this.$           = Fay$$$;
  this.$fayToJs    = Fay$$fayToJs;
  this.$jsToFay    = Fay$$jsToFay;
  

Sollen weitere Funktionen des eigenen Programms nach außen hin sichtbar sein, so ist dies über die Moduldeklaration erreichbar. Die in Klammern hinter dem Modulnamen angegebenen Funktionen werden für den Export ausgewählt.

    module Demo(main, fib) where
    
    fib = ...
    main = ...
  

In diesem Beispiel ist neben der Main-Funktion auch die Funktion "fib" aufrufbar, beispielsweise über die Entwicklerkonsole eines Browsers, wenn das Programm in eine HTML-Seite eingebunden wurde.

Wird beim Kompilieren mit Fay das Flag "--library" gesetzt, so entfällt der automatische Aufruf der Hauptfunktion, zum Beispiel beim Öffnen der Website, die das Skript eingebunden hat. Dies ist relevant, wenn beispielsweise nur eine bestimmte Funktion beim Drücken eines Buttons ausgeführt werden soll.