elixirはrubyっぽい見た目の関数型の言語だよ。
関数型言語といってもelixirは独自の世界観があるからちょっと変わってる。
「関数型だから数学的」ってわけでもなくて、思ったよりわかりやすくて面白いね。
実際に学んでいたのは結構前なんだけど、また学びなおしてみたいなって思ってるよ。
プロセスとGenServer
elixirで特徴的なのはプロセスっていう軽量スレッドがあることだね。
プロセスをたくさん作って、プロセス同士でメッセージをやり取りしてアプリを表現していくよ。
SuperVisorっていうプロセスを管理するものもあって、プロセスが落ちたら再起動するとかいろいろしてくれるよ。
状態を管理するのもプロセスが保持する値を更新することで表現するよ。
Counterを表現するモジュールはこんな感じ
上のほうでインターフェースのようにGenServerのcallやcastの呼び出しを記述してるよ(get_count, add)
それでhandle_callとかhandle_castでそのメッセージを受け取る関数実装して、プロセスが持つ値を取得したり更新したりするよ。
defmodule Counter do
use GenServer
def start_link(initial_value) do
GenServer.start_link(__MODULE__, initial_value)
end
def init(initial_value), do: {:ok, initial_value}
def get_count(pid), do: GenServer.call(pid, :get_count)
def add(pid, value), do: GenServer.cast(pid, {:add, value})
# callは同期的、castは非同期的な処理
def handle_call(:get_count, _from, count), do: {:reply, count, count}
def handle_cast({:add, value}, count), do: {:noreply, count + value}
end
iexで実行するとこんな感じ
start_linkのレスポンスにpidがあるから、それをわたしてget_countやaddを呼び出してるよ。
これだけ見るとあんまりオブジェクト指向と変わらないような感じがするね。
状態を持つデータの参照から状態を取り出しているように見えるね。
逆に言うと適当に書くとelixirっぽいコードにならないような気がするね。
そこを意識して学ぶ必要がありそう。
iex(1){:ok, counter} = Counter.start_link(0)
{:ok, #PID<0.174.0>}
iex(2)> Counter.get_count(counter)
0
iex(3)> Counter.add(counter, 2)
:ok
iex(4)> Counter.get_count(counter)
2
=について
elixirでは=は代入ではなくマッチ演算子だよ。
未定義の変数に使ったときは代入とほぼ同じだからあんまり意識しなくても大丈夫だと思うよ。
あと特に何もしないと再代入みたいな動きになるよ。
a = 1
a = 2 # => 2
これをしないためには変数に^(ピン演算子)をつけるよ。
^a = 2 # => ** (MatchError) no match of right hand side value: 2
あと代入じゃなくてマッチだから1 = a
みたいなことができるよ
a = 1 # => 1
1 = a # => 1
2 = a # => ** (MatchError) no match of right hand side value: 1
[a, a] = [1, 1] #=> [1, 1]
with
MatchErrorしたときにnilを返すようにするにはwithを使うよ。
result = with [a, 2] <- [0, 0], do: a
IO.inspect(result) # => nil
withは、複数マッチさせたいときにも使えるよ。
result =
with {:ok, file} <- File.open("./test.txt"),
contents <- IO.read(file, :eof),
:ok <- File.close(file) do
"Contents: #{contents}"
end
IO.inspect(result) #=> Contents: ファイルの内容
不変性
elixirではリストや構造体などもすべて不変として扱うよ。
リストに新しい値を追加したくなったら、元のリストをコピーして新しいリストを作り直す必要があるよ。
データを直接書き換えるのではなく、データを別の新しいデータに変換して状態を更新していくよ。
Reduxでいうとstateを更新するときと同じような感じで、
rubyでいうとmap!じゃなくてmapを使おうって感じだね。
破壊的なメソッドは使えないということだね。
データをコピーするから、パフォーマンスに問題がありそうに見えるけど、
elixirはデータをコピーすることを前提に作られているから、それほど問題じゃないらしいよ。
新しいリストを作成するときに元のデータの全体、または一部を再利用しているんだって。
関数
fnを使って関数を作れるよ。
無名関数では変数名.()のようにして呼び出せるよ
sum = fn(a, b) -> a + b end
sum.(1, 2) #=> 3
# &を使って省略することもできるよ
sum = &(&1 + &2)
関数の引数もパターンマッチなのでパターンごとに実行内容が書けるよ。
単純にwhenで条件分岐してるような感じだね。
fn
{:ok, value} -> value
{_, error} -> error
end
カリー化もできるよ
greeter = fn name -> fn -> "Hello #{name}" end end
greet_john = greeter.("John")
IO.puts(greet_john.()) #=> Hello John
デフォルト引数は\\
でできるよ
bodyなしで定義することができるよ
def func(a, b \\ "b")
def func(a, b), do: a + b
&についてlist
関数を省略して書くことができる&
ほかにもいろいろできるよ。
関数への参照にも使えるよ
round = &(Float.round(&1, &2)) #=> &Float.round/2
# このように書いても同じ
round = &Float.round/2
# erlangの関数への参照になるのもある
&abs/1 #=> :erlang.abs/1
[]とかとか""とかはelixirの演算子なので関数に変換できるよ
reverse = &{ &2, &1 }
reverse.(10, 5) #=> {5, 10}
string_creator = &"name: #{&1}"
string_creator.("Dave") #=> "name: Dave
for
普通のforはこんな感じ、値が返ってくるのが特徴かな
array = for i <- [1, 2, 3], do: i * 2 #=> [2, 4, 6]
array #=> [2, 4, 6]
構造としてはこんな感じ
for ジェネレーター [, into: value], do:
ジェネレーターは複数書くことができるよ
後ろ側に書いたジェネレーターが先に進む感じ、この例だとyが先に進むね
# x: 1, y: 1 -> x: 1, y: 2 みたいに進む
for x <- [1, 2], y <- [5, 6], do: x * y #=> [5, 6, 10, 12]
フィルターも書くことができるよ
for x <- [1, 2], y <- [5, 6], x * y > 6, do: x * y #=> [10, 12]
loopを表現する末尾再起
elixirはwhileがないので末尾再起でループするよ。
リストの先頭を取り除いていって、空になったら終了する処理はこんな感じ
(命名がちょっと怪しいけど)
defmodule Queue do
# A
def dequeue([]), do: IO.puts("Empty")
# B
# 先頭の値以外がtailに入るよ
def dequeue([_ | tail]) do
IO.puts(inspect(tail))
dequeue(tail)
end
end
実行すると
iex(1)> Queue.dequeue([1,2,3])
[2, 3]
[3]
[]
Empty
:ok
末尾再起は関数を呼び出すときに引数を変更して、関数自体を再帰的に呼ぶことで実現するよ。
この例だとBの部分でリストの先頭を取り除いて再帰的に呼んでいるね。
リストが空になるとAの部分にマッチして終了するよ。
どちらも同じ関数名だけど、引数のパターンによって呼び出される関数が変わることで終了するんだね。
パイプライン
パイプラインは|>
を使って書けるよ
メソッドチェーンみたいな感じだね。
|>
は左の結果を右の関数の第一引数に入れてくれるよ。
[1, 2, 3, 4]
|> Enum.filter(&Integer.is_even/1) #=> [2, 4]
|> Enum.map(&(&1 * 2)) #=> [4, 8]
list.map()
よりEnum.map(list)
のほうが何となくデータが変更されなさそうな感じがするね。
余談
elixirはメッセージングを大切にしていて、
これはオブジェクト指向を考えたアランケイさんの観点に似ているよ。
といっても普段”オブジェクト指向”というとJavaやC++みたいなクラス重視のオブジェクト指向を表すから難しいところだね。
あとがき
ほかにもマクロとか型とかいろいろあるよ。
プロセス周りも奥深い感じだね。
あとelixirはPhoenixというRailsみたいなフレームワークがよくつかわれてる印象だね。
Phoenixはelixirのプロセスが軽い特徴を生かしてStreamとかの状態管理とかも結構得意らしいよ。
この辺りも学びなおしてみたい。