[elixir][erlang] erlang/OTP-R17とelixir
やっとerlang/OTP-R17がリリースされました。UTF-8の全面採用、mapsデータ型の(実験的)サポート、名前つき無名関数など盛りだくさん。
Maps -- Records are dead, long live maps!
いわゆるperlでのハッシュがやっと実装された。これまではproplistモジュールやETSなどで同様のことを行ってきたが、言語そのものに実装されたのはやはり大きい。
> M = #{a => 1, b => 2}. #{a=>1, b=> 2} > #{b := N} = M. #{a=>1, b=> 2} > N. 2
こんな感じで使う。'=>'は新しいキーの割当で':='は既存のキーに対する操作なので、間違えるとエラーになる親切設計。というかimmutableな言語なので当然。
> M = #{a => 1, b => 2}. #{a=>1, b=> 2} > #{c := N2} = M. ** exception error: no match of right hand side value #{a => 1,b => 2} > #{b => N3} = M. * 1: illegal pattern
このようにエラーもきちんと出る。
elixirでは %{}という記法で記述可能。
> a = %{:a => 1, :b => 2} %{a: 1, b: 2} > %{a | b: 3} %{a: 1, b: 3} > %{b: c} = a %{a: 1, b: 2} > c 2
既存のmapsの更新がちょっと気持ち悪いけどまぁいい感じに使えそう。
named fun
名前つきの無名関数とは意味不明だが、lispでいうlabelsに相当するもので、無名関数中で自分自身を参照できるというのがプリティなところ。
> F = fun Fib(0) -> 0; Fib(1) -> 1; Fib(N) -> Fib(N-1) + Fib(N-2) end. #Fun<erl_eval.30.106461118> > F(30). 832040
こんな感じで、fun 名前 (引数) -> 本体 という形で使う。本体中では名前を参照できる。
elixir-v0.12.5ではまだサポートされていない模様。
無名関数での再帰処理
elixirでの無名関数はfnスペシャルフォームで定義できるが、無名関数のため、自分自身を呼び出すような処理は記述できない。
たとえば、フィボナッチ数を求める関数は、
fn (0) -> 0 (1) -> 1 (n) -> self(n-1) + self(n - 2)
のように書くことは出来ない。
Lispでは、そういうときのためにlabelsというスペシャルフォームがあり、
(setf x (labels ((self (n) (cond ((eq n 0) 0) ((eq n 1) 1) (t (+ (self (- n 1)) (self (- n 2))))))) )) (funcall x 10) 55
と書ける。elixirでも同じように書きたいが、selfはpidを返す関数なのでmeという疑似変数を導入して
> a = fn (0) -> 0 (1) -> 1 (n) -> me(n-1) + me(n-2) end >a.(10) > 55
こんなふうに書けたら便利な気がする。
無名関数から自分自身を呼び出す
とりあえず、無名関数を変数に保存しておいて使ってみると、
a = fn(0) -> 0 (1) -> 1 (n) -> a.(n-1) + a.(n-2) end
ではfnの評価時にaが定義されていないので駄目。なので、仮引数としてaを
渡してやれば良い。
b = fn(a) -> fn(0) -> 0 (1) -> 1 (n) -> a.(n-1) + a.(n-2) end end
これを評価すると、無名関数を返す無名関数関数が出来上がる。
b.(b)を評価すると、
> c = b.(b) #Function<6.80484245 in :erl_eval.expr/5> > c.(3) ** (ArithmeticError) bad argument in arithmetic expression :erlang.+(#Function<6.80484245 in :erl_eval.expr/5>, #Function<6.80484245 in :erl_eval.expr/5>)
おお、エラーだ。よく見ると、上のfnではaはbつまり、関数を引数にとることになっているが、上記コードではn-1つまり2を渡している。実際、b.(b)がcつまり、求める無名関数の筈なので、fn中のaをa.(a)に書き換えてみる。
b = fn(a) -> fn(0) -> 0 (1) -> 1 (n) -> a.(a).(n-1) + a.(a).(n-2) end end
これを同様にして評価していくと、
> c = b.(b) #Function<6.80484245 in :erl_eval.expr/5> > c.(3) 2
うまくいった。つまり、アウトラインはこんな感じ。
defmacro afn(function) do cfunc = translate(:me, function) ## function中のmeをme.(me).に変更 quote do me = fn(me) -> unquote(function) end me.(me) end end
マクロとしてまとめる
translate/2はelixirのASTをトラバースしながらパターンを置き換えることになる。
ASTは以下のリテラルから成り立つ。
def ast_traverse(a, _f) when is_atom(a) or is_number(a) or is_binary(a) do a end def ast_traverse({a, b}, _f) do {a, b} end def ast_traverse(a, _f) when is_list(a) do a end
次に、関数呼び出しを含むスペシャルフォームは以下の種類がある。
{a, b, c} when :"->" == a and is_list(c) -> 節 {a, b, c} when is_atom(a) and is_list(c) -> 関数実行 {a, b, c} when is_tuple(a) and size(a) == 3 and is_list(c) -> 関数戻り値を関数として実行 {a, b, c} when is_atom(a) and is_atom(c) -> 変数
それぞれに対応してパターンを記述すると、
def ast_traverse({a, b, c}, f) do {a, b, c} = f.({a, b, c}) case {a, b, c} do {a, b, c} when :"->" == a and is_list(c) -> {a, b, Enum.map(c, fn(x) -> {args, meta, node} = x {Enum.map(args, &(ast_traverse(&1, f))), meta, ast_traverse(node, f)} end)} {a, b, c} when is_atom(a) and is_list(c) -> {ast_traverse(a, f), b, Enum.map(c, &(ast_traverse(&1, f)))} {a, b, c} when is_tuple(a) and size(a) == 3 and is_list(c) -> {ast_traverse(a, f), b, Enum.map(c, &(ast_traverse(&1, f)))} {a, b, c} when is_atom(a) and is_atom(c) -> {ast_traverse(a, f), b, c} end end
これを使ってtranslate/3は以下のように書ける。
def translate(tree, a, module // nil) do ast_traverse(tree, fn({^a, b, c}) when is_list(c) -> {{:., b, [{{:., b, [{a, b, module}]}, b, [{a, b, module}]}]}, b, c} (x) -> x end) end
これでlispのlabelsもどきを実施するdo_label/2とafn/1をやっと定義できるようになった。
def do_label({n, line, module} = nvar, clouse) do {n, line, module} = nvar c = translate(clouse, n, module) c2 = {:fn, line, [{:"->", line, [{[nvar], line, c}]}]} r = quote do me = unquote(c2) me.(me) end r end defmacro afn(clouse) do r = do_label({:me, [], nil}, clouse) r end
これをモジュールMMとかに入れておいて、コンパイルして使ってみる。
iex(106)> c("mm.ex") [MMM, MM] iex(107)> require MM; iex(108)> r = MM.afn(fn (0) -> 0 ...(108)> (1) -> 1 ...(108)> (n) -> me(n-1) + me(n-2) end) #Function<6.80484245 in :erl_eval.expr/5> iex(109)> r.(10) 55 iex(110)>
無名関数の内部でmeという関数名で自分自身を参照できるようになった。
shinjuku.ex#7
久しぶりに開催されたshinjuku.exに参加。今年に入ってからelixir界隈はにぎわってきていて、
Programming Elixir
のような書籍が現れてきた。他にもオライリーからも出版されてきている。
そんな背景と関係あるのか無いのか、今回はKDDIウェブコミュニケーションズが会場であった。
SigilかわいいよSigil
Sigilとはリテラルの拡張にあたるもので、言語組み込みとしては正規表現の%rやバイナリの%bが存在している。elixirではユーザ定義のSigilを使うことができるので今回はそのネタ。
Sigilの定義の仕方
言語で予約されているSigilは以下のとおり
b | バイナリ文字列(エスケープあり, ””と同様) |
B | バイナリ文字列(エスケープなし) |
c | 文字リスト(エスケープあり, ''と同様) |
C | 文字リスト(エスケープなし) |
r | 正規表現(エスケープあり) |
R | 正規表現(エスケープなし) |
w | ワードリスト(エスケープあり) |
W | ワードリスト(エスケープなし) |
コレ以外で文字を選択するのが吉であり、ここではlとする。大文字を小文字にするSigilである。
モジュール中にsigil_lという関数を定義する。こんな感じ。
defmodule Sigill do def sigil_l(a, _opt) do String.downcase(a) end end
このSigilを使う側は、モジュールSigillをimportする必要がある。
iex(1)> import Sigill; IO.puts(%l(ABC)) abc :ok
Elixir中に他の言語を埋め込むための構文として使えそうだ。
ex_doc
elixirは「ドキュメントが第一級のオブジェクト」という面白い概念をもっている。どういう事かというと、プログラムのドキュメントをプログラムから操作できるという事である。doxygenなどは、プログラムのコメントを書式化するものだが、elixirではモジュール属性として設定されているモジュールドキュメントを書式化する。そのためのツールがex_docである。
モジュールドキュメント
@moduledocあるいは、@docによりドキュメントを記述する。
書式はmarkdownがそのまま使えるがインデントのベースが@moduledoc/@docが置かれたレベルであることに注意する。
defmodule M1 do @moduledoc """ これはM1モジュールです。 """ @doc """ func1はnil値を返します。 """ def func1() do nil end end
erlang like record manipulator
rabbitmqのクライアントamqp_clientをelixirで使おうと思ったのだが、erlangのrecordを使い倒していて困った。elixirにもレコードはあるし、erlangレコードをコンバートする事も出来るのだが、-includeを追いかけてくれないとかのelixirのRecord.extract_fromがイマイチだったり、レコードタグ(タプルの最初の要素のアトム)の値をたよりにOK/NGを判定するamqp_clientの仕様が、レコードタグをモジュール名にしてしまうelixirレコードの仕様と合わなかったりしたので、Ermモジュールという名前でマクロライブラリを自作することにした。
Amqpモジュールを例にすると、こんな感じで使う。
defmodule Amqp do use Erm use Amqp.Uri Erm.addpath("dist/rabbit_common*/ebin") :io.format("code ~p~n", [:code.lib_dir(:rabbit_common)]) Erm.defrecords_from_hrl("deps/**/amqp_client*/include/amqp_client.hrl") def start() do {:ok, re} = Amqp.Uri.parse("amqp://user:passwd@amqp-server-host.example.com") :io.format("Refields: ~p~n", [Erm.record_info(:fields, :amqp_params_network)]) :io.format("Re: ~p~n", [re]) {:ok, con} = Amqp.Connection.start(re) {:ok, chan} = Amqp.Connection.open_channel(con) ex = Erm.record(:"exchange.declare", [exchange: "my_exchange", type: "topic"]) Erm.recordl(:"exchange.declare_ok") = Amqp.Channel.call(chan, ex) q = Erm.record(:"queue.declare", [queue: "my_queue"]) Erm.recordl(:"queue.declare_ok") = Amqp.Channel.call(chan, q) binding = Erm.record(:"queue.bind", [queue: "my_queue", exchange: "my_exchange", routing_key: "key"]) Erm.recordl(:"queue.bind_ok") = Amqp.Channel.call(chan, binding) payload = "foobar" publish = Erm.record(:"basic.publish", [exchange: "my_exchange", routing_key: "key"]) p = Erm.record(:"P_basic", [delivery_mode: 2]) p = Erm.record(:"P_basic", p, [delivery_mode: 2]) Amqp.Channel.cast(chan, publish, Erm.record(:amqp_msg, [props: p, payload: payload])) :timer.sleep(10000) get = Erm.record(:"basic.get", [queue: "my_queue", no_ack: true]) {Erm.recordl(:"basic.get_ok"), content} = Amqp.Channel.call(chan, get) Erm.recordl(:amqp_msg, [payload: payload2]) = content :io.format("~p ~p", [payload, payload2]) binding = Erm.record(:"queue.unbind", [queue: "my_queue", exchange: "my_exchange", routing_key: "key"]) Erm.recordl(:"queue.unbind_ok") = Amqp.Channel.call(chan, binding) delete = Erm.record(:"exchange.delete", [exchange: "my_exchange"]) Erm.recordl(:"exchange.delete_ok") = Amqp.Channel.call(chan, delete) delete = Erm.record(:"queue.delete", [queue: "my_queue"]) Erm.recordl(:"queue.delete_ok") = Amqp.Channel.call(chan, delete) :ok = Amqp.Channel.close(chan) :ok = Amqp.Connection.close(con) end end
eppかわいいよepp
erlangのコードを解析してelixirのコードにしてそれをコンパイルするとerlangのコードになるのだが、それは置いておいて、erlangのコードをプリプロセスするためのモジュールeppがあるのでそれを使う。
{:ok, ast} = :epp.parse_file(file, pathlist, opt)
opt, pathlistは何に使うのかイマイチ不明だったのでどちらも[]でOK。fileはbinaryではなくlistであることに注意すると、erlangの抽象構文木が得られる。この構文木をスキャンしてレコード定義部分を抽出してコンバート(Enum.filter_map/3)すれば良い。
レコード定義
レコードはタプルである。従ってタプル定義を登録しておき、あとで名前で参照したときに定義からタプルを生成することができれば良い。
定義情報をマクロのモジュールやメソッドとして保存する方法もあるし、defrecordpとかそのために用意されている節もあるのだが、amqp_clientのレコード名は、P_basicとかqueue.bind_okとか香ばしくてメソッド名に使えないため、etsを使う。
レコード定義は、フィールド名とその初期値のリストのetsテーブルへのinsertであり、レコード定義の参照は、etsテーブルのlookupとなる。
include_libの解決
:epp.parse_fileでは-include_libのファイルオープンエラーは {:error, {_n, :epp, {filepath}}}の形でastに表現される。これを見つけたら例外を発生させればよい。
raise File.Error, reason: :enoent, action: "maybe not search path", path: filepath
といった具合。サーチパスは、:code.add_path('path’)で追加。Path.wildcard("path/**/to/include")のようなかたちでまとめて追加しておくと良い。これはコンパイル時に追加しなければならないので関数定義の外側に置いておく。
astはconvすればいい
さてastから{:attribute, _n, :record, {name, fields}}の形を取り出してetsへ登録すればいいのだが、ここでfieldsの内容がきわめて多様であるためそんな簡単には行かない。まぁ木自体は出来ているので、そこを降りていくだけなのでそんなに難しくはないのだが。
fieldsは初期値のない{:record_field, _n, name}か初期値のある{:record_field, _n, name, value}の形をしている。初期値がない場合には:undefinedを初期値と考えればいいので、そのようにすればいいが、valueについては、任意のerlang項があり得るため、コンバートが必要となる。このコンバート関数をconvとしてどういうパターンがあるか考えてみる。
conv!conv!conv!
{:atom, _n, v} | アトム | v | |
{:integer, _n, v} | 整数 | v | |
{:bin, _n, v} | バイナリ | v | |
{:record, _n, name, fields} | レコード参照 | erec(name, reduce_field(fields)) | |
{:function, func, arity} | 関数 | [:erlang, func, arity] | |
{:function, module, func, arity} | 関数 | [module, func, arity] | |
{:fun, _n, fp} | 無名関数 | {:function, [import: Kernel], conv(fp)} | |
{:cons, _n, car, cdr} | コンス(つまりリスト) | [ conv(car) | conv(cdr)] | |
{:call, _n, mf, args} | 定義時に関数呼び出す | appy(m, f, args) |
といったパターンがある。一文字でない変数で表示した変数は非終端記号であり、さらにconvする必要があるが、上の表の通り、再帰的に書くだけなので問題は無い。面白いのは関数呼び出しや関数オブジェクトも変換対象と出来るのでほぼ全てのerlangの構文をコンバートできてしまっている。
右辺値と左辺値
パターンマッチングでも使いたいが、レコード定義には初期値がもれなく入っているため、レコード参照を仮にErm.record(:record_name)とした時、-record(record_name, {field1})の場合、
Erm.record(:record_name) --> {:record_name, :undefined}
と展開されてしまうため、左辺値としてマッチングに使おうとすると、興味の無いフィールドを明示的に'_'として与えなければならなくなる。これでは使いにくいため、左辺値として使うときには、フィールドのデフォルト値を'_'として展開するようにする。
というところをまとめたのが、
https://github.com/k1complete/erm
になる。。。
韓国への「サイバー攻撃」続報その4
韓国での「サイバー攻撃」の続報その3
ついに原因判明。デスブログに以下のエントリーがあることが判明した。
久しぶりに食べたコリアンは
とってもおいしかったです〜♪
キムチのお土産までいただいて
お腹もいっぱーーい!
攻撃元が中国のIPだったが、実は勝手に韓国の農協内部で使っていたとか、そんなことは結果に過ぎないだろう。
なお、例のIP 101.106.25.105 を逆引きすると 1695750.r.msn.com になってMSのサーバであるというのは、ガセだと思う(実際にやってみればわかること)。
ただし、繰り返すが対処策が時刻を戻してアップデートをしないというのは、やはり怪しい。正規のサーバに対してSP1へアップデートするだけでいい筈。