はじめに
2018年11月、私は名前だけは知っていたElixirを使ってみました。「これはLispだよ!」というのが第一印象です。Lispで仕事をしたいけれどそんな仕事はありそうもない、とお嘆きのLisp使いのために雑文を書きました。
上司との会話
開発担当: この度の案件、ぜひLispでやらせてください。Lispはとても素晴らしい言語です。きっと素晴らしいプロダクトができるはずです。
上司: いや、でも、君ねぇ。仮に素晴らしいプロダクトができたとして、メンテナンスはどうするのだね? Lispを使える人は極めて少ないよ。君が違う部署に異動になったら引き継ぐ者はいないよ。悪いが却下だ。
S式だとLispだってことがすぐにばれてしまいます。しかし、José Valimさんという天才的な若者がアルゴル系言語の見た目のLispを作っちゃったのです。名前をElixirといいます。これはマクロも使えるのでご安心を。Elixirなら実質Lispだなんてことは上司にはわかりません。しかも並列、並行処理に強いので上司も納得するはずです。
Lisp使いのためのElixir入門
前置きが長くなりましたが、いよいよ本題に入ります。
REPL
Lispの良さはというと対話しながらプログラミングしていくところです。REPLを利用して試しながら書いていくと泥んこ玉をこねるようにいつの間にか大きなプログラムが出来上がっています。ElixirもこのREPLがあります。IExと言われています。RubyのIrbと同じようなものです。ターミナルからiexと入力すると起動します。いろいろ試してみましょう。
Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 1+2
3
iex(2)> a = 1
1
iex(3)> a
1
iex(4)>
Lisp同様にElixirにおいても全てが式です。式を入れると何かしら値を返してきます。
関数を定義してみます。defというのを使います。
iex(4)> def foo(x) do x+1 end
** (ArgumentError) cannot invoke def/2 outside module
おや!エラーです。Elixirではすべての関数はモジュール内で定義しないといけません。エディタで書き込んでからloadすることにしましょう。AtomやEmacsなどを使ってコードを書きます。
defmodule Test do
def foo(x) do
x+1
end
end
こういう内容のテキストを作ったらqiita.ex とでもして保存しておきます。さて、REPLであるiexからどうやってloadするのでしょう。下記のようにするとloadされます。
iex(1)> c("qiita.ex")
[Test]
iex(2)>
実行してみましょう。
iex(2)> Test.foo(1)
2
iex(3)
モジュール名は先頭が大文字でないとエラーになるみたいです。モジュール名+ドット+関数、引数です。
Lisp入門ときたら・・・そう、階乗にしましょう。
defmodule Test do
def fact(n) do
if n == 0 do
1
else
n * fact(n-1)
end
end
end
えっ?なんです? 「do end が煩わしい。」ですって。Pascalの begin end より3文字も少ないのですから我慢してください。
再度loadしましょう。ワーニングがでますけど気にしないでください。
iex(3)> c("qiita.ex")
warning: redefining module Test (current version defined in memory)
qiita.exs:1
[Test]
iex(4)> Test.fact(10)
3628800
iex(5)>
次は何にしましょう。「Lispときたら、たらい回し、竹内関数だろう。」って。ごもっともです。
defmodule Test do
def tarai(x,y,z) do
if x <= y do
y
else
tarai(tarai(x-1,y,z),
tarai(y-1,z,x),
tarai(z-1,x,y))
end
end
end
iex(6)> Test.tarai(12,6,0)
12
一瞬で計算を終えました。高速です。あっ?所要時間ですか。time関数がなかったので自分でマクロを使ってつくりました。(詳しいことは下記のページをご参照ください)
https://qiita.com/sym_num/items/4fc0dcfd101d0ae61987
iex(1)> require(Test)
Test
iex(2)> Test.time(Test.tarai(12,6,0))
"time: 109000 micro second"
"-------------"
12
109ミリ秒です。0.109秒。どうです、悪くはないでしょう。そして、驚いたことにS式を使わずにマクロが作れるのです。だんだん、その気になってきましたね。
基本関数
Lispといえばcar、cdr、consなどの基本関数が必須です。これらはElixirではどのようになるのでしょうか?Lispを最初に学んだ頃にやったリストの長さを求める次の関数を例に考えてみます。
(defun my-length (ls)
(if (null ls)
0
(+ 1 (my-length (cdr ls)))))
これをElixirに直訳すると次のようになります。
defmodule Test do
def my_length(ls) do
if ls == [] do
0
else
1 + my_length(cdr(ls))
end
end
def cdr([x|xs]) do xs end
end
iex(3)> Test.my_length([1,2,3])
3
リストは[1,2,3] のように表記されます。空リストは[] と表記されます。
null は x == [] で表されます。Lispなら (eq x '()) と同様です。
cdrはパターンマッチングで表すことができます。[x|xs] はLispでは点対、dot-pairといわれるもので (x . y) のことです。Elixirではパターンマッチングが大活躍します。
iex(5)> [x|y] = [1,2,3]
[1, 2, 3]
iex(6)> x
1
iex(7)> y
[2, 3]
引数として与えられたリストがどういうパターンとマッチするかということに注目した次のような書き方をする方がElixirらしいです。簡潔に書けますね。
defmodule Test do
def my_length([]) do 0 end
def my_length([x|xs]) do
1 + my_length(xs)
end
end
これを読み込むとワーニングがでます。
iex(9)> c("test.ex")
warning: variable "x" is unused
test.ex:6
[Test]
変数xは使われていません。cdr部分だけが再帰計算に必要であり、car部分のxは必要ないのでした。必要のない部分はアンダーバー _ で置き換えることができます。無名変数といいます。これはパターンマッチはしますが使うことがない場合にこれを使います。
そうすると完成版は次のとおりです。
defmodule Test do
def my_length([]) do 0 end
def my_length([_|xs]) do
1 + my_length(xs)
end
iex(12)> Test.my_length([1,2,3])
3
変数とアトム
Lispではシンボルが変数となっています。データとしてシンボルを使う場合にはクオートを付けることとなっています。
> (setq x 1)
1
> x
1
> 'x
X
>
Elixirではデータとして使う場合には : 記号をつけます。
iex(7)> x = 1
1
iex(8)> x
1
iex(9)> :x
:x
空リストと偽
Lispでは空リストも真偽値の偽もnilで表します。しかし、Elixirでは空リストは[]であり、偽はfalseであり、別ものです。Schemeと似ています。Schemeでは空リストは()ですが、偽は#fです。
ex(8)> true
true
iex(9)> false
false
iex(10)> []
[]
iex(11)> false == []
false
無名関数
Lispでいうlambdaです。
(lambda (x) (+ x 1))
のようにわざわざ名前を付けるまでもない一時利用の関数として使っています。Elixirでは次のようになります。
iex(5)> fn(x) -> x+1 end
#Function<6.128620087/1 in :erl_eval.expr/5>
一級市民
ElixirではScheme同様に関数も一級市民として扱われます。
iex(6)> f = fn(x) -> x+1 end
#Function<6.128620087/1 in :erl_eval.expr/5>
iex(7)> f.(2)
3
Common Lisp やISLispでは変数と関数とが違う名前空間で扱われるためfuncallを使います。ElixirではScheme同様に直に関数呼び出しができます。呼び出すときにはドットをつけます。
=はsetqにあらず
x=1 とすると変数xの値が1になるので代入をしているように感じますが、実はパターンマッチングです。次の例を見てください。
iex(2)> x = 1
1
iex(3)> 1 = x
1
連想リスト
Lispでは連想リストが良く使われます。
((one 1) (two 2) (three 3))
> (assoc 'two '((one 1)(two 2)(three 3)))
(TWO 2)
Elixirではキーワードと言われています。
iex(17)> keyword_list = [{:one,1},{:two,2},{:three,3}]
[one: 1, two: 2, three: 3]
iex(18)> keyword_list[:two]
2
{} で表されるものはtupleと言われるものです。Elixirには他にも豊富なデータ型が用意されこれに応じたデータ操作関数が多数、用意されています。
なんとなくLispっぽいって思いませんか?
マクロ
ひと昔前は本物のマクロはS式を使うLisp以外ではあり得ないということが言われていました。しかし、世の中には才能のある人がいるものです。Elixirの設計者はS式を使わない本格的なマクロの方法を考案しました。単純な例で試してみます。
あまり意味はないのですけど、x=x+1 のように変数xの値を1つ増加させるマクロ作ってみます。
(defmacro inc (exp)
`(setq ,exp (+ ,exp 1)))
> (setq x 1)
1
> x
1
> (inc x)
2
> x
2
>
これと同様のことがElixirでも容易にできます。次のコードです。
defmodule Test do
defmacro inc(exp) do
quote do
unquote(exp) = unquote(exp) + 1
end
end
end
iex(2)> require(Test)
Test
iex(3)> x = 1
1
iex(4)> Test.inc(x)
2
iex(5)> x
2
Lispの伝統的マクロと同じような仕組みです。Lispで準クオートを使ってテンプレートを用意していた部分はquoteが対応しています。そのテンプレートの中でも評価されてほしい部分はunquoteをつかってます。今回の場合だと変数名に相当するところです。Test.inc(x) と入力をすると直ちに x=x+1に展開されて実行されます。
以下、マニアックな部分です。Elixirのマクロは与えられる式をtupleとして受け取っています。例えばxは次のようなtupleとなっています。tupleは構文木を表しています。
iex(1)> quote do x end
{:x, [], Elixir}
iex(2)>
テンプレート部分もquoteによりtupleに変換されます。
iex(6)> quote do exp = exp + 1 end
{:=, [],
[
{:exp, [], Elixir},
{:+, [context: Elixir, import: Kernel], [{:exp, [], Elixir}, 1]}
]}
このexp部分に与えられた変数、例えばxを注入します。unquoteによりtupleになっていたデータは評価可能な式に復元されexpの部分と置換されます。そのうえでテンプレートがtupleに変換されて構文木となります。それを評価することにより最終的に x=x+1 として実行されています(と思います、間違っていたら編集リクエストお願いします)。
これ、重要! 試験に出ます。(笑い)
incというマクロはわかりやすい例として使いましたが、この程度のことにマクロを使うべきではないという基本的考え方がElixirにはあります。
Elixirスクールのマクロの解説には次のように書かれていました。
引用開始
注意事項: メタプログラミングはトリッキーで、どうしても必要な場合にのみ使用してください。過度の使用は、ほぼ確実に、理解及びデバッグすることが困難な複雑なコードにつながります。
引用終わり。
健康のためマクロの作りすぎにお気を付けください。(笑い)
並列の時代
CPUの速度は飛躍的に向上してきましたが、近年それは頭打ちになりました。電子回路の物理的な限界に達し、マルチコアCPUが登場してきました。並列処理をすることで速度向上、性能向上を図る方向に向かっています。実はElixirにはその方面における長い歴史があるのです。Elixirは最近の言語ですが、Erlangという1980年代に登場した関数型言語がその土台となっています。エリクソンというスウェーデンの通信機器会社が開発し1990年代にオープンソースとして公開されたのがErlangです。ElixirはErlangの表記を今風のRubyに似たシンタックスにしました。しかし、中身は基本的にはErlangと同様の技術が使われています。
現在ではWEB上で多数のクライアントを相手にしなければならない場合が増加しています。そして安全に処理系が落ちることなく運用されなければなりません。ElixirはErlangの血筋を引いています。フォールトトレランスにおいて極めて優れています。
上司との会話(その後)
開発担当: ぜひともElixirで次のWEB案件を開発させてください。Elixirはフォールトトレランスにおいて優れています。当社がWEBアプリケーションで抜きんでるチャンスです。お願いします。
上司: Elixirだって?(ググる上司)
ああ、それはErlangだね。以前、私も注目していた頃があるよ。そうかElixirはErlangの技術を応用しているのだね。おお、どうやらElixirユーザーは急速に増加しているようだ。特に福岡において急増しているらしい。これならメンテナンス要員の問題もないな。
よろしい、その案件、Elixirでやってみたまえ。
開発担当: ありがとうございます。がんばります。
(内心)
(ムフフ、やったね。Elixirで仕事ができる。Lispで毎日遊んでお給料をもらうのが夢だったんだ。さあ、楽しい毎日になるね。 今夜は乾杯だ)
参考文献
Elixirスクール(日本語版)
https://elixirschool.com/ja/
Elixirの作者、José Valimへのインタビュー
https://qiita.com/HirofumiTamori/items/441cb854967b46ca2055
Erlang ウィキペディア
https://ja.wikipedia.org/wiki/Erlang