Hlavní navigace

Propojení světa LISPu se světem JavaScriptu s využitím transpřekladače Wisp (2.část)

8. 12. 2015
Doba čtení: 25 minut

Sdílet

Ve druhé části článku o transpřekladači Wisp si ukážeme, jakým způsobem lze implementovat obdobu programových smyček s využitím rekurze a TCO. Dále si popíšeme práci s proměnnými, práci s poli a vektory i to, jak se provádí překlad jmen deklarovaných funkcí do JavaScriptu. Nezapomeneme ani na tvorbu maker.

Obsah

1. Klasická rekurze a některá její omezení

2. Rekurze a TCO (tail call optimization)

3. Proměnné a mutátory

4. Pole a vektory

5. Překlad jmen funkcí do JavaScriptu

6. Volání metod a funkcí JavaScriptu

7. Tvorba uživatelských maker

8. Tvorba jednoduchých maker s využitím defmacro

9. Použití „syntax-quote“ a „unquote“ při tvorbě uživatelských maker

10. Repositář s dnešními demonstračními příklady

11. Odkazy na předchozí části tohoto seriálu

12. Odkazy na Internetu

1. Klasická rekurze a některá její omezení

Dostáváme se k typické vlastnosti společně většině programovacích jazyků založených či odvozených od LISPu. Wisp totiž, podobně jako mnohé další (nejenom) LISPovské programovací jazyky, preferuje rekurzi před masivním používáním programových smyček. Jsou pro to samozřejmě dobré důvody, jak teoretické, tak i praktické (opět jde o paralelní výpočty). Ve Wispu lze většinou použít přímý zápis rekurze, tj. v těle vytvářené funkce se může objevit volání této funkce. Zcela typickým „školním“ příkladem rekurzivní funkce je funkce pro výpočet faktoriálu, jejíž jednoduchá varianta (neochráněná před všemi typy vstupů) může vypadat takto:

(defn fact
    [n]
    (if (<= n 1)
        1
        (* n (fact (- n 1)))))

Tato funkce je do JavaScriptu přeložena podle očekávání:

var fact = exports.fact = function fact(n) {
    return n <= 1 ? 1 : n * fact(n - 1);
};

Přílišnému nadšení nad tím, jak jednoduše nyní můžeme spočítat faktoriál z libovolně velkého čísla, však nepodléhejme, protože může dojít (a dojde) ke dvěma problémům: přetečení výsledku (do kladného nekonečna) a hlavně k přetečení zásobníku. Pojďme si to vyzkoušet. Nejdříve vytvoříme privátní funkci pro výpočet faktoriálu, ať se nemusíme zabývat problémy s objektem exports (privátní funkce se deklaruje pomocí defn- a nikoli defn):

(defn- fact
    [n]
    (if (<= n 1)
        1
        (* n (fact (- n 1)))))

Tato rekurzivně definovaná funkce je do JavaScriptu přeložena opět podle očekávání:

var fact = function fact(n) {
    return n <= 1 ? 1 : n * fact(n - 1);
};

Transpřeloženou funkci uloženou do souboru fact.js použijeme na HTML stránce:

<html>
    <head>
        <title>factorial</title>
        <script type="text/javascript" src="fact.js">
        </script>
    </head>
    <body>
        <script>
            document.write(fact(10));
            document.write("<br/>");
            document.write(fact(100));
            document.write("<br/>");
            document.write(fact(1000));
            document.write("<br/>");
            document.write(fact(10000));
            document.write("<br/>");
            document.write(fact(100000));
        </script>
    </body>
</html>

Podívejme se nyní na výsledek i na chybovou konzoli:

První dva výpočty jsou korektní, u dalších dvou výpočtů byl překročen maximální rozsah typu double použitého v JavaScriptu (v Clojure by se naproti tomu mohlo přejít na čísla typu BigDecimal) a konečně u posledního výpočtu došlo k přetečení zásobníku, na nějž se ukládají mezivýpočty a návratové body z volající funkce. Právě zde spočívá největší problém klasických rekurzivně volaných funkcí – velikost zásobníku je a vždy bude omezená, navíc i samotné volání rekurzivní funkce představuje poměrně vysokou režii.

2. Rekurze a TCO (tail call optimization)

Nyní již víme, že důvod, proč předchozí volání funkce fact skončilo s chybou, spočívá v tom, že došlo k přeplnění zásobníku při rekurzivním volání. Na zásobník se totiž musí ukládat parametry předávané volané funkci a taktéž body návratu (zjednodušeně řečeno návratové adresy). Aby k přetečení zásobníku nedocházelo, můžeme naši funkci fact upravit tak, aby se využívalo takzvané tail rekurze. Velmi zjednodušeně řečeno je tail rekurze použita tehdy, pokud je posledním příkazem nějaké funkce příkaz pro rekurzivní volání té samé funkce. V tomto případě se nemusí na zásobník nic ukládat a namísto toho se prostě provede skok. V Clojure a tím pádem taktéž ve Wispu se však musí tail rekurze zapsat explicitně, což má své přednosti i zápory (podle mě převažují přednosti, protože již ze zápisu programu je zcela zřejmé, kdy k tail rekurzi skutečně dojde).

Na základě informací, které jsme se dozvěděli v předchozím textu, se tedy pokusme upravit původní čistě rekurzivní způsob zápisu funkce pro výpočet faktoriálu takovým způsobem, aby bylo možné využít tail rekurzi. Funkci je nutné upravit tak, aby jejím posledním příkazem bylo opět volání fact – u původní verze tomu tak nebylo, protože posledním příkazem bylo násobení. První pokus o úpravu spočívá v zavedení akumulátoru výsledku:

(defn fact
    [n acc]
    (if (<= n 1)
        acc
        (fact (- n 1) (* acc n))))

Ovšem stále je zde jeden problém – z původní funkce s jedním parametrem se nyní stala funkce, jíž je nutné předávat i druhý parametr, který navíc musí být nastavený na jedničku. Náprava je prostá a spočívá v použití funkce fact s volitelnou aritou (popř. by bylo možné vytvořit pomocnou funkci, ovšem mě se následující zápis líbí více, protože se zbytečně nevytváří pomocné funkce):

(defn- fact
    ([n]
     (fact n 1))
    ([n acc]
     (if (<= n 1)
         acc
         (fact (- n 1) (* acc n)))))

Výše uvedená funkce již může využívat tail rekurze, ovšem jak jsme si již řekli, je nutné tail rekurzi zapsat explicitně. Proto i zde dojde k chybě při přetečení zásobníku (na rozdíl od mnoha LISPů).

Explicitní zápis rekurze spočívá ve využití speciální formy recur, která se zapíše přesně do místa, kde má k tail rekurzi (=skoku) dojít:

(defn- fact
    ([n]
     (fact n 1))
    ([n acc]
     (if (<= n 1)
         acc
         (recur (-n 1) (* acc n)))))

Poznámka: tuto funkci nedokáže současná verze Wispu přeložit.

Poměrně jednoduše lze rekurzi s využitím TCO deklarovat pomocí loop společně s recur. Za formou loop se nachází vektor s deklarací a inicializací lokálních proměnných použitých ve smyčce, na kterou je rekurzivní volání automaticky převedeno. Forma recur umístěná uvnitř loop přesune řízení programu ihned za slovo loop, přičemž parametry předané do recur slouží ke změně hodnot(y) lokálních proměnných. Co to znamená v praxi? Podívejme se na způsob zápisu smyčky, jejíž počitadlo (lokální proměnná i) se zvyšuje od 0 do 10:

; počitadlo od 0 do 10
(loop [i 0]
    (if (== i 10)          ; podmínka pro ukončení smyčky
        sum                ; návratová hodnota při splnění podmínky
        (recur (+ i 1))))  ; rekurze (s TCO)
                           ; v nové iteraci obsahuje i hodnotu staré_i+1

Smyčka, ve které se používá počitadlo i a současně se počítá i suma sum, bude implementována takto:

; výpočet desáté mocniny dvojky
(loop [i 0 sum 1]
    (if (== i 10)          ; podmínka pro ukončení smyčky
        sum                ; návratová hodnota při splnění podmínky
        (recur (+ i 1) (* sum 2))))  ; rekurze (s TCO)
                           ; v nové iteraci obsahuje i hodnotu staré_i+1
                           ; a sum hodnotu staré_sum * 2

Podívejme se nyní na implementaci funkce pro výpočet n-té mocniny dvojky:

; funkce pro výpočet n-té mocniny dvojky
(defn pow2
    [n]
    (loop [i 0 sum 1]
        (if (== i n)       ; podmínka pro ukončení smyčky
            sum            ; návratová hodnota při splnění podmínky
            (recur (+ i 1) (* sum 2)))))  ; rekurze (s TCO)
                           ; v nové iteraci obsahuje i hodnotu staré_i+1
                           ; a sum hodnotu staré_sum * 2

Další příklady na použití forem loop a recur:

; příklad pro výpočet sumy pole
(def array [1 2 3 4 5])
 
(loop [i 0 sum 0]
    (if (== i 5)           ; podmínka pro ukončení smyčky
        sum                ; návratová hodnota při splnění podmínky
        (recur (+ i 1) (+ sum (get array i) ))))  ; rekurze (s TCO)
 
; explicitní TCO?
(defn power
    ([x y]
        (power x y 1))
    ([x y current]
        (if (== y 0)
            current
            (if (> y 0)
                (recur x (- y 1) (* x current))
                (recur x (+ y 1) (/ current x))))))
 
(defn power2
    [x y]
    (loop [exponent y
           current 1.0]
        (if (== exponent 0)
            current
            (if (> exponent 0)
                (recur (- exponent 1) (* x current))
                (recur (+ exponent 1) (/ current x))))))

Výsledek transpřekladu do JavaScriptu:

// implementace počitadla od 0 do 10
// povšimněte si lokálních symbolů uvnitř funkce
// a použití loop pro předávání parametrů
(function loop() {
    var recur = loop;
    var iø1 = 0;
    do {
        recur = iø1 == 10 ? sum : (loop[0] = iø1 + 1, loop);
    } while (iø1 = loop[0], recur === loop);
    return recur;
}.call(this));
// vlastní smyčka je skutečně převedena na smyčku, zde typu do-while
 
 
 
// implementace počitadla od 0 do 10
// společně s výpočtem sumy
// lokální proměnné opět mají v názvu speciální znak ø
(function loop() {
    var recur = loop;
    var iø1 = 0;
    var sumø1 = 1;
    do {
        recur = iø1 == 10 ? sumø1 : (loop[0] = iø1 + 1, loop[1] = sumø1 * 2, loop);
    } while (iø1 = loop[0], sumø1 = loop[1], recur === loop);
    return recur;
}.call(this));
 
 
 
// funkce pro výpočet n-té mocniny dvojky
var pow2 = exports.pow2 = function pow2(n) {
    return function loop() {
        var recur = loop;
        var iø1 = 0;
        var sumø1 = 1;
        do {
            recur = iø1 == n ? sumø1 : (loop[0] = iø1 + 1, loop[1] = sumø1 * 2, loop);
        } while (iø1 = loop[0], sumø1 = loop[1], recur === loop);
        return recur;
    }.call(this);
};
 
 
 
// zpracování polí/vektorů ve funkci
var array = exports.array = [
    1,
    2,
    3,
    4,
    5
];
 
(function loop() {
    var recur = loop;
    var iø1 = 0;
    var sumø1 = 0;
    do {
        recur = iø1 == 5 ? sumø1 : (loop[0] = iø1 + 1, loop[1] = sumø1 + (array || 0)[iø1], loop);
    } while (iø1 = loop[0], sumø1 = loop[1], recur === loop);
    return recur;
}.call(this));
 
 
 
// výpočet obecné mocniny
var power = exports.power = function power() {
    switch (arguments.length) {
    case 2:
        var x = arguments[0];
        var y = arguments[1];
        return power(x, y, 1);
    case 3:
        var x = arguments[0];
        var y = arguments[1];
        var current = arguments[2];
        return y == 0 ? current : y > 0 ? (loop[0] = x, loop[1] = y - 1, loop[2] = x * current, loop) : (loop[0] = x, loop[1] = y + 1, loop[2] = current / x, loop);
    default:
        throw RangeError('Wrong number of arguments passed');
    }
};
 
 
 
// výpočet obecné mocniny, druhá možnost implementovaná pomocí loop a recur
var power2 = exports.power2 = function power2(x, y) {
    return function loop() {
        var recur = loop;
        var exponentø1 = y;
        var currentø1 = 1;
        do {
            recur = exponentø1 == 0 ? currentø1 : exponentø1 > 0 ? (loop[0] = exponentø1 - 1, loop[1] = x * currentø1, loop) : (loop[0] = exponentø1 + 1, loop[1] = currentø1 / x, loop);
        } while (exponentø1 = loop[0], currentø1 = loop[1], recur === loop);
        return recur;
    }.call(this);
};

3. Proměnné a mutátory

Na rozdíl od programovacího jazyka Clojure, v němž jsou implicitně všechny datové struktury neměnné (immutable), je tomu v případě jazyka Wisp poněkud jinak, protože Wisp se snaží o co nejužší kooperaci s JavaScriptem, který používá běžné proměnné. I z tohoto důvodu sice existuje možnost vytvoření globální proměnné či datové struktury s využitím speciální formy def, ovšem kdykoli později je možné hodnotu změnit pomocí set!. Vykřičník je součástí názvu, což se ostatně týká i dalších funkcí a speciálních metod, které mění stav programu. Podívejme se nyní na několik příkladů, v nichž se def a set! používá:

; Ukázka použití proměnných.
 
 
; Pozor: proměnné nejsou ve Wispu neměnitelné (immutable)
; tak jako je tomu v originálním Clojure!
(def x 42)
 
(def y (+ x 1))
 
(def z [1 2 3 4])
 
; mutátor
(set! x (- x 1))
 
; speciální formu if je možné použít i uvnitř výrazu
(set! y (if (< x 0) "negative" "positive"))
 
; změna typu hodnoty uložené do proměnné
(set! x [1 2 3 4])
(set! x "Hello World!")
(set! x 0)
(set! x true)
(set! x nil)
 
; proměnné jsou přístupné i uvnitř funkcí
(defn foo
    []
    (+ x 1))
 
; proměnné versus lokální symboly
(defn foo
    []
    (print x)
    (let [x 42]
        (print x)))
 
; proměnné versus lokální symboly
(defn foo
    []
    (print x)
    (let [x 42]
        (let [x 99999]
            (print x))
        (print x)))

Výsledek transpřekladu do JavaScriptu je velmi přímočarý:

var x = exports.x = 42;
 
var y = exports.y = x + 1;
 
var z = exports.z = [
    1,
    2,
    3,
    4
];
 
x = x - 1;
 
y = x < 0 ? 'negative' : 'positive';
 
x = [
    1,
    2,
    3,
    4
];
 
x = 'Hello World!';
 
x = 0;
 
x = true;
 
x = void 0;
 
var foo = exports.foo = function foo() {
    return x + 1;
};
 
var foo = exports.foo = function foo() {
    console.log(x);
    return function () {
        var xø1 = 42;
        return console.log(xø1);
    }.call(this);
};
 
var foo = exports.foo = function foo() {
    console.log(x);
    return function () {
        var xø1 = 42;
        (function () {
            var xø2 = 99999;
            return console.log(xø2);
        }.call(this));
        return console.log(xø1);
    }.call(this);
};

Pokud nepotřebujete symboly proměnných exportovat, lze při jejich deklaraci použít metainformaci :private:

; lokální proměnné v rámci modulu
(def ^:private xx 42)
 
(def ^:private yy (+ x 1))
 
(def ^:private zz [1 2 3 4])

Překlad do JavaScriptu bude odlišný – bude chybět přiřazení do struktury exports:

var xx = 42;
var yy = x + 1;
var zz = [
    1,
    2,
    3,
    4
];

4. Pole a vektory

Další datovou strukturou, která je jak v JavaScriptu, tak i přeneseně ve Wispu, měnitelná (mutable), jsou vektory či pole. V programovacím jazyku Clojure jsou vektory, stejně jako další typy sekvencí, striktně neměnné, ovšem existuje zde možnost použít nativní Javovská pole či vícedimenzionální pole. Pro přístup k prvkům těchto polí resp. pro modifikaci prvků, se používají funkce aget a aset (kupodivu bez vykřičníku). V jazyku Wisp se příliš mezi neměnitelnou sekvencí a polem nerozlišuje, což znamená, že funkci aget i makro aset lze použít právě pro vektory, a to následujícím způsobem:

; Ukázka práce s poli (vektory).
 
 
; Pole == vektory nejsou neměnné (immutable) tak jako v Clojure!
(def pole [1 2 3 4])
 
; pouze výraz pro přečtení prvku pole
(aget pole 1)
 
; přečtení vybraného prvku z pole
(def x (aget pole 1))
 
; změna hodnoty prvků v poli
(aset pole 0 1000)
(aset pole 2 "Hello")
 
; alternativní způsob
(set! (aget pole 2) 999)
 
 
; dvourozměrné pole (matice)
(def matice [[1 2 3] [4 5 6] [7 8 9]])
 
; nepravidelná matice
(def matice2 [[1] [2 3] [4 5 6] [7 8 9 10]])

Výsledek transpřekladu do JavaScriptu:

// Pole == vektory nejsou neměnné (immutable) tak jako v Clojure!
var pole = exports.pole = [
    1,
    2,
    3,
    4
];
 
// pouze výraz pro přečtení prvku pole
pole[1];
 
// přečtení vybraného prvku z pole
var x = exports.x = pole[1];
 
// změna hodnoty prvků v poli
pole[0] = 1000;
pole[2] = 'Hello';
pole[2] = 999;
 
// dvourozměrné pole (matice)
var matice = exports.matice = [
    [
        1,
        2,
        3
    ],
    [
        4,
        5,
        6
    ],
    [
        7,
        8,
        9
    ]
];
 
// nepravidelná matice
var matice2 = exports.matice2 = [
    [1],
    [
        2,
        3
    ],
    [
        4,
        5,
        6
    ],
    [
        7,
        8,
        9,
        10
    ]
];

Opět platí, že lze deklarovat i lokální proměnné (v rámci jednoho modulu):

; Pole == vektory nejsou neměnné (immutable) tak jako v Clojure!
(def ^:private pole [1 2 3 4])
 
; dvourozměrné pole (matice)
(def ^:private matice [[1 2 3] [4 5 6] [7 8 9]])
 
; nepravidelná matice
(def ^:private matice2 [[1] [2 3] [4 5 6] [7 8 9 10]])

Což se přeloží následovně:

var pole = [
    1,
    2,
    3,
    4
];
var matice = [
    [
        1,
        2,
        3
    ],
    [
        4,
        5,
        6
    ],
    [
        7,
        8,
        9
    ]
];
var matice2 = [
    [1],
    [
        2,
        3
    ],
    [
        4,
        5,
        6
    ],
    [
        7,
        8,
        9,
        10
    ]
];

5. Překlad jmen funkcí do JavaScriptu

Vzhledem k tomu, že programovací jazyky založené na LISPu používají zcela odlišné konvence pro pojmenování funkcí a globálních proměnných (resp. přesněji řečeno symbolů), než je tomu v jazycích, od nichž je odvozen JavaScript, musí zákonitě při kooperaci mezi takto odlišnými světy docházet k problémům při porozumění programovému kódu. Překladač Wisp se tomu snaží zabránit, a to takovým způsobem, že některé jmenné konvence použité ve zdrojovém kódu detekuje a nahradí je konvencemi známými z JavaScriptu. Podívejme se na několik typických příkladů:

  1. Funkce s víceslovním názvem: calculate-multiplication
  2. Predikát, tj. funkce vracející pravdivostní hodnotu: zero?
  3. Funkce provádějící konverzi: string->bool
  4. Privátní funkce: **hidden** (jedná se pouze o jmennou konvenci!)

Wisp provede následující přejmenování:

  1. Funkce s víceslovním názvem: calculateMultiplication
  2. Predikát, tj. funkce vracející pravdivostní hodnotu: isZero
  3. Funkce provádějící konverzi: stringToBool
  4. Privátní funkce: __hidden__

Ostatně se o tom můžeme jednoduše přesvědčit sami:

; Jména funkcí generovaná transpřekladačem Wisp
 
 
; Běžná funkce zapisovaná ve stylu LISPu
(defn calculate-multiplication
    [x y]
    (* x y))
 
 
; Predikáty
(defn zero?
    [x]
    (== x 0))
 
(defn even?
    [x]
    (zero? (mod x 2)))
 
(defn odd?
    [x]
    (not (even? x)))
 
 
; Konverzní funkce
(defn string->bool
    [s]
    (== s "true"))
 
(defn deg->rad
    [angle]
    (* angle (/ 3.1415 180)))
 
 
; Privátní funkce
(defn **hidden**
    [x]
    (+ x 1))

Výsledek transpřekladu do JavaScriptu:

var calculateMultiplication = exports.calculateMultiplication = function calculateMultiplication(x, y) {
    return x * y;
};
 
var isZero = exports.isZero = function isZero(x) {
    return x == 0;
};
 
var isEven = exports.isEven = function isEven(x) {
    return isZero(x % 2);
};
 
var isOdd = exports.isOdd = function isOdd(x) {
    return !isEven(x);
};
 
var stringToBool = exports.stringToBool = function stringToBool(s) {
    return s == 'true';
};
 
var degToRad = exports.degToRad = function degToRad(angle) {
    return angle * (3.1415 / 180);
};
 
var __hidden__ = exports.__hidden__ = function __hidden__(x) {
    return x + 1;
};

6. Volání metod a funkcí JavaScriptu

Podobně jako programovací jazyk Clojure nabízí takzvaný Java-interop, tedy vzájemnou interoperabilitu mezi Clojure a Javou (volání konstruktorů a metod, přístup k atributům), musí jazyk Wisp zajistit interoperabilitu s JavaScriptem. Je to pochopitelný požadavek, protože je velmi pravděpodobné, že program napsaný ve Wispu bude muset například měnit DOM HTML dokumentu, volat JavaScriptové funkce a metody node.js serveru atd. Jaké operace tedy musí Wisp zajistit?:

  1. Volání konstruktorů JavaScriptových objektů
  2. Volání metod JavaScriptových objektů
  3. Přístup k atributům JavaScriptových objektů

Podívejme se, jak jsou tyto vlastnosti ve Wispu implementovány. Můžeme zde vidět nepatrné odlišnosti od jazyka Clojure a to především při přístupu k atributům:

; Interoperabilita mezi Wispem a JavaScriptem
 
 
; konstruktory
(new Date)
 
(new Date "December 7, 2015 14:16:18")
 
; alternativní zápis konstruktorů
; (nezapomeňte na tečku na konci názvu objektu!)
(Date.)
 
(Date. "December 7, 2015 14:16:18")
 
; volání metod
 
(def str "Hello world!")
 
(.indexOf str "!")
 
; alternativní zápis volání metod
(str.indexOf "!")
 
; uložení výsledků volání metod
(def x (.random Math))
(def y (Math.random))
 
; přístup k atributům je oproti Javě+Clojure poněkud komplikovanější
(def pole [1 2 3])
(.-length pole)

Výsledek transpřekladu do JavaScriptu:

new Date();
 
new Date('December 7, 2015 14:16:18');
 
new Date();
 
new Date('December 7, 2015 14:16:18');
 
var str = exports.str = 'Hello world!';
 
str.indexOf('!');
 
str.indexOf('!');
 
var x = exports.x = Math.random();
 
var y = exports.y = Math.random();
 
var pole = exports.pole = [
    1,
    2,
    3
];
 
pole.length;

7. Tvorba uživatelských maker

Jedna z velmi silných stránek programovacího jazyka Wisp je podpora pro práci s makry. Makra jsou aplikována ihned při načítání forem ze standardního vstupu, popř. při načítání jednotlivých forem ze zdrojového souboru. Pro jednoduchost si můžeme představit, že tato makra provádí textové substituce, i když tyto substituce mohou být obecně složitější (a mnohem užitečnější), než například substituce prováděné preprocesorem programovacího jazyka C či C++, a to zejména z toho důvodu, že reader makra používají informace získané z interpretru jazyka Wisp.

Význam uživatelských maker spočívá především v tom, že se tato makra provedou (aplikují) při zpracování zadávaných forem těsně předtím, než jsou tyto formy transformovány do JavaScriptu. To má dva důsledky: makro je v daném místě programu vykonáno pouze jednou a navíc makro může pracovat přímo s uživatelem zadanou formou bez toho, aby se daná forma automaticky vyhodnotila. To mj. znamená, že s využitím uživatelských maker lze realizovat i speciální formy (o nichž jsme se zmínili minule), například nové typy programových smyček apod.

8. Tvorba jednoduchých maker s využitím defmacro

V následujícím textu se budeme snažit vytvořit poměrně jednoduché makro nazvané trace sloužící k tomu, aby se při jeho použití vypisovaly na konzoli trasovací informace o tom, jaký výraz (forma) je volána a jaká je návratová hodnota této formy po vyhodnocení. Toto makro by tedy mělo být při zápisu:

(trace vyraz)

…expandováno (v první velmi nedokonalé verzi) na následující formu:

(let [x vyraz]
    (print "vyraz = " x)
    x)

Důvod, proč je využita lokální proměnná je jednoduchý – hodnotu vyhodnoceného výrazu musíme vypsat a současně i vrátit (což je ono x na posledním řádku); navíc by nebylo správné výraz vyhodnocovat dvakrát kvůli vedlejším efektům. Za vyraz je přitom možné dosadit jakýkoli platný výraz akceptovaný transpřekladačem Wisp, což znamená, že po zápisu:

(trace (* 6 7))

By se měla ve skutečnosti vytvořit (v compile time) a následně v čase běhu (runtime) vyhodnotit tato forma:

(let [x (* 6 7)]
    (print "(* 6 7)=" x)
    x)

Resp. pravděpodobně spíše následující forma:

(let [x (* 6 7)]
    (print (quote (* 6 7)) "=" x)
    x)

(připomeňme si, že quote se obvykle zapisuje pomocí apostrofu, což však zde nebude možné, protože předchozí výraz budeme konstruovat jako seznam a nikoli zapisovat z klávesnice).

Vygenerování výrazu odpovídajícího požadovanému chování trace vlastně není příliš složité, když si připomeneme, jak se vytváří příkazy. Stačí si uvědomit, že apostrofy zakážou vyhodnocení symbolu, který je zapsán ihned za apostrofem a funkce list slouží k vytvoření seznamu z prvků, které jsou funkci list předány, tj. zápis (list a b c) vrátí seznam (a b c):

(defn gen-trace [vyraz]
    (list
        'let ['x vyraz]
        (list 'print (list 'quote vyraz) "=" 'x)
        'x))

Zbývá udělat další krok – vytvořit skutečné uživatelské makro. To je velmi jednoduché, protože postačuje namísto defn (definice funkce) použít defmacro (definice makra). Výsledkem je objekt aplikovaný v čase zadávání nových forem, což nám umožní předávat makru výrazy bez nutnosti jejich quotování (uvození pomocí znaku apostrof nebo quote):

(defn gen-trace [vyraz]
    (list
        'let ['x vyraz]
        (list 'print (list 'quote vyraz) "=" 'x)
        'x))
 
↓ ↓ ↓
↓ ↓ ↓
↓ ↓ ↓
 
(defmacro trace [vyraz]
    (list
        'let ['x vyraz]
        (list 'print (list 'quote vyraz) "=" 'x)
        'x))

Otestování funkce makra:

(defmacro trace [vyraz]
    (list
        'let ['x vyraz]
        (list 'print (list 'quote vyraz) "=" 'x)
        'x))
 
(trace 42)
 
(trace (Math.random))
 
(trace "Hello")
 
(trace (* 6 7))

Výsledek transpřekladu do JavaScriptu:

// (trace 42)
void 0;
(function () {
    var xø1 = 42;
    console.log(42, '=', xø1);
    return xø1;
}.call(this));
 
// (trace (Math.random))
(function () {
    var xø1 = Math.random();
    console.log(list(symbol(void 0, 'Math.random')), '=', xø1);
    return xø1;
}.call(this));
 
// (trace "Hello")
(function () {
    var xø1 = 'Hello';
    console.log('Hello', '=', xø1);
    return xø1;
}.call(this));
 
// (trace (* 6 7))
(function () {
    var xø1 = 6 * 7;
    console.log(list(symbol(void 0, '*'), 6, 7), '=', xø1);
    return xø1;
}.call(this));

9. Použití „syntax-quote“ a „unquote“ při tvorbě uživatelských maker

Podívejme se ještě jednou na výraz, na který jsme chtěli expandovat zápis (trace vyraz):

(let [x vyraz]
    (print (quote vyraz) " = " x)
    x)

Abychom vytvořili tento výraz ve formě rekurzivně zanořeného seznamu, definovali jsme makro trace s následující podobou:

(defmacro trace [vyraz]
    (list
        'let ['x vyraz]
        (list 'print (list 'quote vyraz) "=" 'x)
        'x))

Toto makro sice v rámci možností funguje (až na několik chybiček), ovšem způsob jeho zápisu a vlastně i způsob jeho vytvoření je, eufemicky řečeno, poměrně nehezký :-) Proč tomu tak je je zřejmé – ve vytvářeném seznamu jsme museli všechny funkce quotovat, seznam se musel explicitně vytvářet s využitím funkce list atd. Bylo by určitě mnohem zajímavější, jednodušší a současně i elegantnější mít možnost zápisu makra ve formě jakési šablony, do níž by se pouze dosazovaly části původního výrazu předaného makru pro expanzi. Požadavek na snadnou formu makra pomocí „šablony“ ve skutečnosti není nijak nový, protože se začal objevovat již záhy po vytvoření programovacího jazyka LISP, tj. před zhruba padesáti lety. Tvůrci různých variant LISPu se tomuto požadavku snažili vyhovět různými způsoby; my si v následujícím textu řekneme, jak je tomu v případě Wisp. Vylepšený – a nutno říci, že prozatím nefunkční – tvar makra trace může vypadat následovně:

(defmacro trace2 [vyraz]
  `(let [x ~vyraz] (print '~vyraz "=" x) x))

Co se vlastně změnilo? Již zde nepoužíváme nepěkný způsob vytváření seznamu s využitím funkcí list a quotovaných jmen volaných funkcí a speciálních forem (konkrétně let, print a quote). Namísto toho je požadovaný výsledný tvar makra jednoduše zapsán a před tento zápis je vložen znak zpětného apostrofu, který se přepíše (expanduje) jako makro „syntax-quote“ Ovšem pouze s aplikací „syntax-quote“ se daleko nedostaneme, protože ve skutečnosti potřebujeme hned na dvou místech do makra vložit vstupní výraz. Aby to bylo možné, je uvnitř „syntax-quote“ použita tilda, neboli reader makro nazvané „dequote“, které pro nejbližší následující symbol ruší funkci quotování.

Výsledkem je toto makro:

(defmacro trace
    [vyraz]
    `(let [x ~vyraz]
        (print '~vyraz "=" x)
        x))

Povšimněte si, jak je toto makro pěkně čitelné; až na ~ a ` se vlastně podobá běžné funkci. Zpětný apostrof zajistí, že se vše za apostrofem (celá forma až do konce makra) bude zpracovávat v takové podobě, jak je makro deklarováno, ovšem s jedinou výjimkou – vyraz je nahrazen skutečným parametrem makra, ať již se jedná o číslo, řetězec, složitou formu či další makro.

Wisp navíc zajistí, že lokální proměnná x bude mít unikátní jméno, což je oproti Clojure nepatrné vylepšení (tam se musel používat další symbol #).

10. Repositář s dnešními demonstračními příklady

Demonstrační příklady, na nichž jsme si ukazovali další vlastnosti transpřekladače Wisp, byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/clojure-examples. V tabulce zobrazené pod tímto odstavcem naleznete na zdrojové kódy těchto příkladů přímé odkazy:

Poznámka 1: soubory 7–11 vznikly překladem prvních pěti zdrojových skriptů.

root_podpora

Poznámka 2: zdrojové kódy s demonstračními příklady mají, podobně jako v předchozím článku, koncovku .clj, takže dojde ke správnému „obarvení“ při otevření těchto souborů v programátorských editorech. Transpřekladači Wisp na koncovce nezáleží, i když se v dokumentaci doporučuje použít koncovku .wisp.

Poznámka 3: soubor Makefile je možné použít pro překlad všech zdrojových souborů do JavaScriptu s využitím příkazu make. Při změně obsahu se provede překlad jen změněných souborů. Pokud potřebujete smazat soubory generované překladačem, lze použít příkaz make clean.

11. Odkazy na předchozí části tohoto seriálu

  1. Leiningen: nástroj pro správu projektů napsaných v Clojure
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/
  2. Leiningen: nástroj pro správu projektů napsaných v Clojure (2)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-2/
  3. Leiningen: nástroj pro správu projektů napsaných v Clojure (3)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-3/
  4. Leiningen: nástroj pro správu projektů napsaných v Clojure (4)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-4/
  5. Leiningen: nástroj pro správu projektů napsaných v Clojure (5)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-5/
  6. Leiningen: nástroj pro správu projektů napsaných v Clojure (6)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-6/
  7. Programovací jazyk Clojure a databáze (1.část)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-databaze-1-cast/
  8. Pluginy pro Leiningen
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-pluginy-pro-leiningen/
  9. Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi/
  10. Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi (2)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi-2/
  11. Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk
    http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk/
  12. Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (2)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-2/
  13. Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (dokončení)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-dokonceni/
  14. Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure
    http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure/
  15. Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (2)
    http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-2/
  16. Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (3)
    http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-3/
  17. Programovací jazyk Clojure a práce s Gitem
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem/
  18. Programovací jazyk Clojure a práce s Gitem (2)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem-2/
  19. Programovací jazyk Clojure – triky při práci s řetězci
    http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-retezci/
  20. Programovací jazyk Clojure – triky při práci s kolekcemi
    http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-kolekcemi/
  21. Programovací jazyk Clojure – práce s mapami a množinami
    http://www.root.cz/clanky/programovaci-jazyk-clojure-prace-s-mapami-a-mnozinami/
  22. Programovací jazyk Clojure – základy zpracování XML
    http://www.root.cz/clanky/programovaci-jazyk-clojure-zaklady-zpracovani-xml/
  23. Programovací jazyk Clojure – testování s využitím knihovny Expectations
    http://www.root.cz/clanky/programovaci-jazyk-clojure-testovani-s-vyuzitim-knihovny-expectations/
  24. Programovací jazyk Clojure – některé užitečné triky použitelné (nejenom) v testech
    http://www.root.cz/clanky/programovaci-jazyk-clojure-nektere-uzitecne-triky-pouzitelne-nejenom-v-testech/
  25. Enlive – výkonný šablonovací systém pro jazyk Clojure
    http://www.root.cz/clanky/enlive-vykonny-sablonovaci-system-pro-jazyk-clojure/
  26. Nástroj Leiningen a programovací jazyk Clojure: tvorba vlastních knihoven pro veřejný repositář Clojars
    http://www.root.cz/clanky/nastroj-leiningen-a-programovaci-jazyk-clojure-tvorba-vlastnich-knihoven-pro-verejny-repositar-clojars/
  27. Propojení světa LISPu se světem JavaScriptu s využitím transpřekladače Wisp
    http://www.root.cz/clanky/propojeni-sveta-lispu-se-svetem-javascriptu-s-vyuzitim-transprekladace-wisp/

12. Odkazy na Internetu

  1. Wisp na GitHubu
    https://github.com/Gozala/wisp
  2. Wisp playground
    http://www.jeditoolkit.com/try-wisp/
  3. REPL v prohlížeči
    http://www.jeditoolkit.com/in­teractivate-wisp/
  4. Minification (programming)
    https://en.wikipedia.org/wi­ki/Minification_(programmin­g)
  5. Clojure Macro Tutorial (Part I, Getting the Compiler to Write Your Code For You)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-i-getting.html
  6. Clojure Macro Tutorial (Part II: The Compiler Strikes Back)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-ii-compiler.html
  7. Clojure Macro Tutorial (Part III: Syntax Quote)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-ii-syntax.html
  8. Tech behind Tech: Clojure Macros Simplified
    http://techbehindtech.com/2010/09/28/clo­jure-macros-simplified/
  9. Fatvat – Exploring functional programming: Clojure Macros
    http://www.fatvat.co.uk/2009/02/clo­jure-macros.html
  10. Eulerovo číslo
    http://cs.wikipedia.org/wi­ki/Eulerovo_??slo
  11. List comprehension
    http://en.wikipedia.org/wi­ki/List_comprehension
  12. List Comprehensions in Clojure
    http://asymmetrical-view.com/2008/11/18/list-comprehensions-in-clojure.html
  13. Clojure Programming Concepts: List Comprehension
    http://en.wikibooks.org/wi­ki/Clojure_Programming/Con­cepts#List_Comprehension
  14. Clojure core API: for macro
    http://clojure.github.com/clo­jure/clojure.core-api.html#clojure.core/for
  15. cirrus machina – The Clojure for macro
    http://www.cirrusmachina.com/blog/com­ment/the-clojure-for-macro/
  16. Clojure.org: Clojure home page
    http://clojure.org/downloads
  17. Clojure.org: Vars and the Global Environment
    http://clojure.org/Vars
  18. Clojure.org: Refs and Transactions
    http://clojure.org/Refs
  19. Clojure.org: Atoms
    http://clojure.org/Atoms
  20. Clojure.org: Agents as Asynchronous Actions
    http://clojure.org/agents
  21. A Couple of Clojure Agent Examples
    http://lethain.com/a-couple-of-clojure-agent-examples/
  22. Clojure – Functional Programming for the JVM
    http://java.ociweb.com/mar­k/clojure/article.html
  23. Clojure quick reference
    http://faustus.webatu.com/clj-quick-ref.html
  24. 4Clojure
    http://www.4clojure.com/
  25. ClojureDoc
    http://clojuredocs.org/
  26. Clojure (Wikipedia EN)
    http://en.wikipedia.org/wiki/Clojure
  27. Clojure (Wikipedia CS)
    http://cs.wikipedia.org/wiki/Clojure
  28. Riastradh's Lisp Style Rules
    http://mumble.net/~campbe­ll/scheme/style.txt
  29. Dynamic Languages Strike Back
    http://steve-yegge.blogspot.cz/2008/05/dynamic-languages-strike-back.html
  30. Scripting: Higher Level Programming for the 21st Century
    http://www.tcl.tk/doc/scripting.html
  31. Java Virtual Machine Support for Non-Java Languages
    http://docs.oracle.com/ja­vase/7/docs/technotes/gui­des/vm/multiple-language-support.html
  32. The Lua VM, on the Web
    https://kripken.github.io/lu­a.vm.js/lua.vm.js.html
  33. Lua.vm.js REPL
    https://kripken.github.io/lu­a.vm.js/repl.html
  34. lua2js
    https://www.npmjs.com/package/lua2js
  35. lua2js na GitHubu
    https://github.com/basicer/lua2js-dist
  36. Seriál o programovacím jazyku Lua
    http://www.root.cz/serialy/pro­gramovaci-jazyk-lua/
  37. Source-to-source compiler
    https://en.wikipedia.org/wiki/Source-to-source_compiler
  38. JavaScript is Assembly Language for the Web: Sematic Markup is Dead! Clean vs. Machine-coded HTML
    http://www.hanselman.com/blog/Ja­vaScriptIsAssemblyLanguage­ForTheWebSematicMarkupIsDe­adCleanVsMachinecodedHTML­.aspx
  39. JavaScript is Web Assembly Language and that's OK.
    http://www.hanselman.com/blog/Ja­vaScriptIsWebAssemblyLangu­ageAndThatsOK.aspx
  40. Dart
    https://www.dartlang.org/
  41. CoffeeScript
    http://coffeescript.org/
  42. TypeScript
    http://www.typescriptlang.org/
  43. Lua (programming language)
    http://en.wikipedia.org/wi­ki/Lua_(programming_langu­age)
  44. Static single assignment form (SSA)
    http://en.wikipedia.org/wi­ki/Static_single_assignmen­t_form
  45. LuaJIT 2.0 SSA IRhttp://wiki.luajit.org/SSA-IR-2.0
  46. The LuaJIT Project
    http://luajit.org/index.html
  47. LuaJIT FAQ
    http://luajit.org/faq.html
  48. LuaJIT Performance Comparison
    http://luajit.org/performance.html
  49. LuaJIT 2.0 intellectual property disclosure and research opportunities
    http://article.gmane.org/gma­ne.comp.lang.lua.general/58908
  50. LuaJIT Wiki
    http://wiki.luajit.org/Home
  51. LuaJIT 2.0 Bytecode Instructions
    http://wiki.luajit.org/Bytecode-2.0
  52. Programming in Lua (first edition)
    http://www.lua.org/pil/contents.html
  53. Lua 5.2 sources
    http://www.lua.org/source/5.2/
  54. Tcl Plugin Version 3
    http://www.tcl.tk/software/plugin/
  55. JavaScript: The Web Assembly Language?
    http://www.informit.com/ar­ticles/article.aspx?p=1856657
  56. asm.js
    http://asmjs.org/
  57. List of languages that compile to JS
    https://github.com/jashke­nas/coffeescript/wiki/List-of-languages-that-compile-to-JS
  58. REPL
    https://en.wikipedia.org/wi­ki/Read%E2%80%93eval%E2%80%93prin­t_loop
  59. The LLVM Compiler Infrastructure
    http://llvm.org/ProjectsWithLLVM/
  60. clang: a C language family frontend for LLVM
    http://clang.llvm.org/
  61. emscripten
    http://kripken.github.io/emscripten-site/
  62. LLVM Backend („Fastcomp“)
    http://kripken.github.io/emscripten-site/docs/building_from_source/LLVM-Backend.html#llvm-backend
  63. Emscripten – Fastcomp na GitHubu
    https://github.com/kripken/emscripten-fastcomp
  64. Clang (pro Emscripten) na GitHubu
    https://github.com/kripken/emscripten-fastcomp-clang
  65. Why not use JavaScript?
    https://ckknight.github.i­o/gorillascript/

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.