近年、ネットワークアプリケーションを書くにあたり Erlang/OTP に対する注目が集まっています。 大規模サービスで使わているなどの事例が多く紹介されるようになり、導入を考えている方も多いでしょう。 ですが、Erlang は少々癖があり、難しく感じる方も少なくありません。 今回はそのような人向けに LFE を紹介しようと思います。
LFE とは (lisp (flavored (erlang))) の略です。その名の通り、 Erlang Virtual Machine (BEAM) 上で動く Lisp です。
LFE はそれなりに古く、Robert Virding 氏が 2007 年頃にプロトタイプを開発開始し、R12B の時に初リリースされています。
それからしばらく更新されていましたが一時開発が止まっているような状態になっていました。(私にはそのように見えた)
ですが、近年 Duncan McGreggor 氏などが参画し、再度開発が活発化し lfetool
やドキュメントなどが整備され始めています。
現在も Github 上で開発が行われています。
LFE とはどんなものでしょうか?LFE の特徴は以下です。
- Erlang VM で動作する
- Erlang とシームレスに統合
- S 式で記述(マクロも含め)
- Common Lisp スタイルのドキュメントをサポート
- Lisp-2
- REPLのサポート
基本的には Erlang と何も変わりません。Erlang VM で動作しているので当たり前です。 Erlang とシームレスに統合されているため、Erlang で出来ることは全て可能です。 パターンマッチング、レコードでのパターンマッチング、ガードも出来ます。 もちろん OTP も使用出来ます。また言語的に変な癖も特にありません。 そのため、通常の Erlang コードから乗り換えることも容易です。
以下は LYSE でのサンプルコードです。Erlang と LFE で比較してみます。
-module(kitty_gen_server).
-export([start_link/0, order_cat/4, return_cat/2, close_shop/1]).
-record(cat, {name, color=green, description}).
%%% Client API
start_link() -> spawn_link(fun init/0).
%% Synchronous call
order_cat(Pid, Name, Color, Description) ->
Ref = erlang:monitor(process, Pid),
Pid ! {self(), Ref, {order, Name, Color, Description}},
receive
{Ref, Cat} ->
erlang:demonitor(Ref, [flush]),
Cat;
{'DOWN', Ref, process, Pid, Reason} ->
erlang:error(Reason)
after 5000 ->
erlang:error(timeout)
end.
%% This call is asynchronous
return_cat(Pid, Cat = #cat{}) ->
Pid ! {return, Cat},
ok.
%% Synchronous call
close_shop(Pid) ->
Ref = erlang:monitor(process, Pid),
Pid ! {self(), Ref, terminate},
receive
{Ref, ok} ->
erlang:demonitor(Ref, [flush]),
ok;
{'DOWN', Ref, process, Pid, Reason} ->
erlang:error(Reason)
after 5000 ->
erlang:error(timeout)
end.
%%% Server functions
init() -> loop([]).
loop(Cats) ->
receive
{Pid, Ref, {order, Name, Color, Description}} ->
if Cats =:= [] ->
Pid ! {Ref, make_cat(Name, Color, Description)},
loop(Cats);
Cats =/= [] -> % got to empty the stock
Pid ! {Ref, hd(Cats)},
loop(tl(Cats))
end;
{return, Cat = #cat{}} ->
loop([Cat|Cats]);
{Pid, Ref, terminate} ->
Pid ! {Ref, ok},
terminate(Cats);
Unknown ->
%% do some logging here too
io:format("Unknown message: ~p~n", [Unknown]),
loop(Cats)
end.
%%% Private functions
make_cat(Name, Col, Desc) ->
#cat{name=Name, color=Col, description=Desc}.
terminate(Cats) ->
[io:format("~p was set free.~n",[C#cat.name]) || C <- Cats],
ok.
LFE では以下のようになります。
(defmodule kitty-gen-server
(export all)
(behavior gen_server))
(defrecord cat
name
(color 'green)
description)
(defun start-link ()
(gen_server:start_link (MODULE) '() '()))
(defun order-cat (pid name color description)
(gen_server:call pid `#(order ,name ,color ,description)))
(defun return-cat
((pid (= (match-cat) cat))
(gen_server:cast pid `#(return ,cat))))
(defun close-shop (pid)
(gen_server:call pid 'terminate))
(defun init
(('()) `#(ok ,())))
(defun handle_call
((`#(order ,name ,color ,description) _from cats)
(if (=:= cats '())
`#(reply ,(make-cat name name color color description description) ,cats)
`#(reply ,(hd cats) ,(tl cats))))
(('terminate _from cats)
`#(stop normal ok ,cats)))
(defun handle_cast
((`#(return ,(= (match-cat) cat)) cats)
`#(noreply ,(cons cat cats))))
(defun handle_info (msg cats)
(io:format "Unexpected message ~p~n" `(,msg))
`#(noreply ,cats))
(defun terminate
(('normal cats)
(lc ((<- cat cats))
(leave-msg cat))
'ok))
(defun leave-msg
(((match-cat name name))
(io:format "~p was set free.~n" `(,name))))
(defun code_change (_oldvsn state _extra)
`#(ok ,state))
LFE 固有のところが少しあるかも知れませんが、基本的な部分は Erlang と同様です。 むしろスッキリ書けてるとこすらあります。
Erlang とほぼ同様ならば LFE を使用する利点は何でしょうか?
- Lisp である
これ一択といっていいでしょう。Lisp での特徴である S 式を使用したメタプログラミングを強力に推し進めることができます。
同様に Lisp ベースのものでは joxa, lol などもあります。 こちらも最初は勢いがあったのですが、だいぶ廃れてしまっています。 lol は LFE に近いですが Lisp-1 を採択しています。
Duncan McGreggor といえば一部の人からは非同期厨として知られています。 そうです、かの divmod のメンバーの一人でもあります。Twisted など非同期フレームワークのエキスパートと言えるでしょう。 この手のプログラマーが行き着くとこはやはり Erlang といった感じでしょうか。
それでは LFE をインストールします。 公式ドキュメントにのっとってインストールします。
$ git clone https://github.com/rvirding/lfe.git
$ cd lfe
$ make compile
$ make install
インストール先を変更したい場合には以下のコマンドでインストールします。
$ make install DESTBINDIR=/home/ma2/bin
正常にインストールができているか確認します。lfe
コマンドを実行して REPL が立ち上がるか確認します。
Erlang/OTP 17 [erts-6.4] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
LFE Shell V6.4 (abort with ^G)
>
LFE にはサポートツールとして lfetool
というものがあります。
使い方などは割愛しますが、lfetool
が提供している機能の一部を以下に挙げます。
lfe
およびその関連ツールの のインストールlfetool
のアップデート、依存のアップデート- アプリケーションの雛形の作成
- テスト実行
- REPL の実行
lfetool
自体のコマンドはプラグインになっているので拡張していく事も可能です。
雛形作成やテスト実行は重宝しますのでインストールしておきましょう。
インストール方法は以下です。
$ curl -L -o ./lfetool https://raw.github.com/lfe/lfetool/master/lfetool
$ bash lfetool install
$ lfetool -x
https://github.com/lfe/lfetool
lfe
コマンドを実行します。すると LFE Shell (REPL) が立ち上がります。
$ lfe
Erlang/OTP 17 [erts-6.4] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
LFE Shell V6.4 (abort with ^G)
>
REPL での操作は基本的に erl
での REPL と同じです。
REPL は関数の挙動を調べたり、調査のために小さなコード片を実行したりといったり、マクロを展開するなど開発時にはとても重宝します、
> (+ 1 1 1 1)
4
> (defun double (x) (* x 2))
double
> (set x 3)
3
> (double x)
6
> (set f (lambda (x) (* x x)))
#Fun<lfe_eval.12.2018457>
> (funcall f 3)
9
>
lfetool
であれば以下のコマンドで REPL が立ち上がります。
$ lfetool repl
それでは次に LFE の基本的な部分を見ます。まずは基本的な構文、データ型などを見ていきます。 また前にも書きましたが、LFE には変な癖がありません。 LFE になろうともデータ型などは Erlang のものと同じです。
LFE は Lisp なので S 式で記述します。
(defmodule trade-fsm
(export all)
(behavior gen_fsm))
(defrecord state
(name '"")
other
(ownitems '())
(otheritems '())
monitor
from)
(defmacro monitor% (pid)
`(monitor 'process ,pid))
(defun init (name)
`#(ok idle ,(make-state name name)))
(defun notice
(((match-state name name) str args)
(let ((fmt (string:join `("~s: " ,str "~n") "")))
(io:format fmt (cons name args)))))
LFE は Common Lisp からかなり影響を受けており、 かなり近いものになっています。
defun
, defmacro
, let
など多くを Common Lisp スタイルの form
をサポートしています。
一部 Scheme スタイルもサポートしていますがあまり使われていません。
コードフォーマットも一般的な Commom Lisp スタイルで記述します。 但し、グローバルな値(関数ですが)の命名は LFE だと耳あてしない (aaa のような変数名) というケースも見受けられます。
それではまずは LFE でも特別扱いになっているシンタックスから見ていきます。 特別なシンタックスルールは以下です。
シンタックス | 定義 |
---|---|
#b #o #d #x #23r | Intger (基数指定) |
#(e e ... ) | Tuple |
#b(e e ... ) | Binary |
#m(k v ... ) | Map |
[ ... ] | ( ... ) と同じ |
数は少なくシンプルです。但し、特別なシンタックスで tuple
などを作成した場合には
少し評価を意識する必要があります。
> #(a b)
#(a b)
> (set x 1)
1
> #(a x)
#(a x)
> `#(a ,x)
#(a 1)
> (tuple 'a x)
#(a 1)
>
このように #(e e ... )
では内部が評価されずそのまま atom
扱いになります。
そのため、このシンタックスを使う場合は後述する Backquote と組み合わせて使うのが一般的です。
もちろん無理に使う必要はありません。通常の tuple
関数でタプルを作成しても構いません。
次に主要な form
を簡単に紹介します。多くは special form
と呼ばれる特殊形式になります。
quote
(quote e)
cons
(quote e)
LFE の場合、Erlang と異なり、大文字小文字を区別する必要がありません、
任意の名前をつけることができます。また LFE は Lisp 扱いのため、 Lisp 系の命名規則で
記述します。注意する点は単語をつなげる際には -
でつなげるという点です。
例:
(defun start-link
((name limit sup mfa) (when (and (is_atom name) (is_integer limit)))
(gen_server:start_link `#(local ,name) (MODULE) `#(,limit ,mfa ,sup) '())))
(defun start_link (name limit sup mfa)
(start-link name limit sup mfa))
但し、-
であると behavior のコールバックが呼ばれないので、上記のようにラップしたものを
定義するケースも多く見受けられます。
命名なども含め、LFE にはスタイルガイドがあります。参考にしてみてください。
あまり Lisp に馴染みのない方もいるかも知れません。簡単に Backquote Macro につい て触れておきます。Backquote は後続のS式評価を止めます。内部も評価されません。
以下簡単な例です。
> (set x 1)
1
> x
1
> (list x 2)
(1 2)
> `(x 2)
(x 2)
>
x
に 1 をセットし、その値が評価され、1 として扱われるか見ます。
(list x 2)
は評価され、(1 2)
が表示されました。
``(x 2)は
(x 2)` とそのまま `x` が atom 扱いになります。
さらに実行してみます。
> `(,x 2)
(1 2)
>
,
を x
に前に記述します。,
を使うとその変数が評価され、(1 2)
が表示されました。
backquote 内で ,
を使うとその後続の式が評価されます。もう少し例を書きます。
> (set msg "lfe !")
"lfe !"
> (io:format "hello ~s~n" `(,msg))
hello lfe !
ok
> (io:format "hello ~s~n" `(msg))
hello msg
ok
> `(1 2 (tuple 1 2))
(1 2 (tuple 1 2))
> `(1 2 ,(tuple 1 2))
(1 2 #(1 2))
>
上記のように Backquote と ,
を使うことでどの部分を評価し、どの部分を評価しない
かをコントロールすることが出来ます。すなわち、コードのテンプレートを作ることが出来るのです。
そのため、マクロを定義する場合などでよく使用されます。
(defmacro caar (x) `(car (car ,x)))
その他にも、tuple
, list
, の省略化にも使われます。特に tuple
部はよく使用されます。
(defun handle_cast
((`#(return ,(= (match-cat) cat)) cats)
`#(noreply ,(cons cat cats))))
LFE で扱えるデータ型をみます。 基本的に Erlang と何も変わりません。
関数定義は Common Lisp と同様です。