URL
試した環境
- Ubuntu Server 14.04 LTS
- Erlang/OTP 18
- Elixir 1.0.4
導入
モジュールに複数の関数を入れてグループ化する。
これまでにいくつかのモジュールを見てきた。
例えばStringモジュールはこんな感じ。
iex> String.length "hello"
5
自分でモジュールを作るには、defmoduleマクロを使う。
そのマクロの中でdefマクロを使って関数を定義する。
iex> defmodule Math do
...> def sum(a, b) do
...> a + b
...> end
...> end
{:module, Math,
<<70, 79, 82, 49, 0, 0, 4, 124, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 123, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 2, 104, 2, ...>>,
{:sum, 2}}
iex> Math.sum(1, 2)
3
Compilation
モジュールをファイルへ書くことのメリットの多くは、それがコンパイルできて再利用できるところにある。
ファイル名がmath.exで、次の内容内容を持っているとする。
defmodule Math do
def sum(a, b) do
a + b
end
end
このファイルは、elexirc
を使ってコンパイルできる
$ eliexirc math.ex
$ ls
Elixir.Math.beam math.ex
こうすることで、定義されたモジュールのバイトコードを含むElixir.Math.beamという名前のファイルが生成される。
もう一度iexを開始すると、定義したモジュールが使えるようになる。(iexを開始した沖に同じディレクトリにあるバイトコードのファイルは自動的に読み込まれる。)
iex(1)> Math.sum(1,2)
3
Elixirのプロジェクトは通常3つのディレクトリで構成される。
- ebin - コンパイルされたバイトコードが入る
- lib - Elixirのコードが入る(.exファイル)
- test - テストが入る(.exsファイル)
実際のプロジェクトはmixというビルドツールがコンパイルと適切なパスのセッティングをうまくやってくれる。
学習目的のために、Elixirではコンパイルしたbeamファイルを再生せずに、柔軟性があるスクリプトモードというものも使える。
Scripted mode
.ex
というElixirの拡張子の他に、スクリプティングのために .exs
ファイルもサポートしている。
Elixirは2つのファイルを同じように扱う。違いは意図していることだけ。 .ex
ファイルはコンパイルされること意図しているが、 .exs
ファイルはコンパイルがなしでスクリプトとして使われる。例えば、math.exs
というファイルを作成することができる。
defmodule Math do
def sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2)
そして、次のように実行できる
$ elixir math.exs
3
math.exsファイルは、メモリ内でコンパイルされ、実行され、結果として3を表示する。バイトコードファイルは作られない。
今後の例を実行するような場合は、スクリプトファイルに書いて実行すると良い。
Named functions
モジュールの中で、def/2を使って関数を定義したり、defp/2を使ってプライベート関数を定義できる。
def/2で定義した関数は別のモジュールから呼び出せるのに対し、プライベート関数はモジュール中からしか呼び出せない。
defmodule Math do
def sum(a, b) do
do_sum(a, b)
end
defp do_sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2) #=> 3が返ってくる
IO.puts Math.do_sum(1, 2) #=> UndefinedFunctionErrorになる
関数宣言はガードと複数の節が使える。もし関数に複数の節がある場合は、マッチする節があるまで順番に試す。引数が数値の0かそうでないかをチェックする関数を書くと次のようになる。
defmodule Math do
def zero?(0) do
true
end
def zero?(x) when is_number(x) do
false
end
end
IO.puts Math.zero?(0) #=> true
IO.puts Math.zero?(1) #=> false
引数がどの節にもマッチしない場合は、エラーが発生する。
IO.puts Math.zero?([1,2,3]) #=> FunctionClauseErrorになる
Function capturing
これまで関数を表現するのに、name/arityという記法を使いましたが、実はこの記法は、関数型として名前付き関数を取得するために使用することができる。
iexを開始して、math.exsで試すと次のようになる。
$ iex math.exs
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function fun
true
iex> fun.(0)
true
iex> fun.(1)
false
iex> fun.([1,2,3])
** (FunctionClauseError) no function clause matching in Math.zero?/1
math.exs:2: Math.zero?([1, 2, 3])
is_function/1
のようなローカル、もしくはインポートされた関数はモジュールを付けなくても、関数をキャプチャできる。
iex> fun2 = &is_function/1
&:erlang.is_function/1
iex> fun2.(fun)
true
キャプチャ構文は関数定義のためのショートカットとしても使える。
iex> fun = &(&1 + 1)
#Function<6.54118792/1 in :erl_eval.expr/5>
iex> fun.(1)
2
&1は関数に渡された最初の引数を表す。上記の&(&1 + 1)は、fn x -> x + 1 end と同じ。この構文は短い関数定義をするときに有用。キャプチャ演算子&についてはKernel.SpecialForms
のドキュメントで詳しく知ることができる。
Default arguments
名前付き関数では、デフォルト引数をサポートしている。
defmodule Concat do
def join(a, b, sep \\ " ") do
a <> sep <> b
end
end
IO.puts Concat.join("Hello", "world") #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
デフォルト値としてどんな式でも用意しておけるが、関数定義の時に評価されない。単に後で使うものとして格納されるだけ。関数が呼ばれる時で、その関数のデフォルト値が利用されるたびに、デフォルト値に設定した式が評価される。
defmodule DefaultTest do
def dowork(x \\ IO.puts "hello") do
x
end
end
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
hello
:ok
もし、デフォルト値つきの関数に複数の節がある場合、デフォルト値を宣言するヘッダ部分を作っておくことを推奨する。
defmodule Concat do
def join(a, b \\ nil, sep \\ " " ) # この部分
def join(a, b, _sep) when is_nil(b) do
a
end
def join(a, b, sep) do
a <> sep <> b
end
end
IO.puts Concat.join("Hello", "world") #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello") #=> Hello
もしデフォルト値を使うなら、関数定義の上書きについては十分な注意が必要。
次のような例がある。
defmodule Concat do
def join(a, b) do
IO.puts "***First join"
a <> b
end
def join(a, b, sep \\ "") do
IO.puts "***Second join"
a <> sep <> b
end
end
上記のコードをコンパイルすると、次のようなワーニングが表示される。
$ elixirc concat.ex
concat.ex:7: warning: this clause cannot match because a previous clause at line 2 always matches
2引数のjoin関数は常に最初のjoinの定義を呼び出すことになり、2番目のものは3つの引数が渡された時にしか呼びされないことをコンパイラが教えてくれる。
iex(1)> Concat.join("Hello", "world")
***First join
"Helloworld"
iex(2)> Concat.join("Hello", "world", "_")
***Second join
"Hello_world"
モジュールの簡単な導入はおしまい。