LispでJSXを書いてみる


はじめに

LispベースのDSLを定義できるJavaScriptのライブラリー 「Liyad」v0.0.3 をリリースしました。
Liyadには、JSXをLispで記述する 「LSX」 の演算子セットがバンドルされています。

※「LSX」の名前は今回のリリース時に付けました。

TL;DR

LispでJSXを書く(LSX)のは最高に使いやすいので広めていきたい。
Playgroundはこちら

トランスパイルの辛さ

React等でビューを書く場合、JSXをトランスパイルするのが辛い、と思ったことはありませんか?

大きなプロダクトを作っているのならば、JSXだけではくTypeScript等もトランスパイルするかもしれないので、我慢ならなくても諦めることになります(爆遅なので常時watchすることになりますが、それでも遅い)。

しかし、2~3画面作るだけのために、ボイラープレートからプロジェクトのひな型をコピーして、コードを少し直す度にビルドするのは大袈裟過ぎるように思えます。Vue.jsの人気が上がるのも頷けます。

Template Literalでコンパイルする

幸いにもES6のTemplate Literalはユーザー定義の関数に文字列と値の配列を渡すことができ、戻り値は何でも許されます。

function foo(strings: TemplateStringsArray, values: any[]) {
    // strings === ['Hello, ', '!']
    // values  === ['World']
    return 12345;
}
foo`Hello, ${"World"}!`; // === 12345

この仕組みを使えば、文法的に自然な形でJSX(的なもの)を記述できそうです。

JSX出門

あなたは本当にJSXを愛していたのでしょうか?
繰り返しや条件を書くために {} でぶつ切りにしなければならないJSXを、
JavaScript側に戻らなければ簡単な共通化もできないJSXを、
愛していたといえますか?

Reactでは、そしてその関数呼び出しにコンパイルされるJSXでは純粋(数学的な意味ではない)性を重視する思想がありますが、本当に上記の問題と純粋性は共存できないのでしょうか?
他の選択肢を考えるのであれば贅沢を言いたくなります。

Alt-JSX としてのLisp

LispのS式は括弧だらけというイメージから敬遠されがちですが、HTMLやXMLのような階層構造を持つリストデータの表現形式としては相性が良いのではないかと思います。

(div (@ (id "12345")
        (style (width "100px")
               (height "200px") )
        (class ("foo" "bar" "baz")) )

    (b "Hello,") "World!
    Good morning!"
)

以外に見やすいと思いませんか?
また、Lispは関数型言語であり、すべての括弧(リスト)は原則、関数呼び出しになります。
つまり、上記のタグや属性の処理は自然に関数として処理できます。

Lispであれば、括弧内のどこにでも条件や繰り返しの関数を組み入れることができます。
また、関数を定義すれば複数コンポーネントの局所的な共通化を図ることもできます。

以下に、今回リリースした
LispベースのDSLを定義できるJavaScriptのライブラリー 「Liyad」のサンプルコードを記述します。
特に意味のある描画はしていませんが、見やすさや利便性は感じられるのではないでしょうか。

($defun X (name)
    (Template
        (Hello (@ (key name)
            (name ($concat "John Smith " name)) ))
        (div "Good morning!")
    )
)

($defun Y ()
    (Template
        (select (@ (style (display "inline-block")
                          (width "300px") )
                   (className "foo bar baz")
                   (onChange ${(e) => this.handleSelected(e.target.value)}) ) ;; \${}部分はJavaScript側の式です。

            ($=for ${[{name:"aaa"}, {name:"bbb"}]}                            ;; \${}部分はJavaScript側の式です。
                ($=if (== (% $index 2) 1)
                    (option (@ (value $index)) ($concat "odd: " ($get $data "name")) )
                )
                ($=if (== (% $index 2) 0)
                    (option (@ (value $index)) ($concat "even: " ($get $data "name")) )
                )
            )
        )
        (Button (@ (variant "contained")
                   (color "primary")
                   (onClick ${(e) => this.handleClick(e.target.value)}) )     ;; \${}部分はJavaScript側の式です。
            "hello")
    )
)

($let initial-data ($list 1 2 3 4 5))

(Template
    ($=for initial-data
        (Hello (@ (key $data)
            (name ($concat "Jane Doe " $data)) ))
        (X $data)
    )
    (Y)(br)
    (Y)
)

以上のLispコードを、以下の lsx... のところに記載します。
(テキストハイライトの都合で分割しています)

const Hello = (props) =>
    React.createElement('div', {},
        `Hello, ${props.name}, and Lisp!`,
        ...(Array.isArray(props.children) ? props.children : [props.children])
    );

const components = {Hello};

// material-ui を利用可能にします。
// material-ui はscriptタグで読み込まれており、グローバルに公開されています。
for (const x of Object.keys(window["material-ui"])
                .filter(x => typeof window["material-ui"][x] === 'function' &&
                             window["material-ui"].hasOwnProperty(x))) {

    components[x] = window["material-ui"][x];
}

const lsx = liyad.LSX({
    jsx: React.createElement,
    jsxFlagment: React.Fragment,
    components,
});

ReactDOM.render(lsx`...`, document.getElementById('root'));

実行結果