モジュールとコンパイル,スクリプト

cf. 8 モジュール – Modules – Elixir

モジュール

Elixir では関数をモジュールの中に入れてグループ化する。モジュールは defmodule マクロで定義し、その中で def マクロを使って関数を定義する。またマクロって単語が出てきたけど気にしない。

iex(1)> defmodule Math do
...(1)>   def sum(a, b) do
...(1)>     a + b
...(1)>   end
...(1)> end
{:module, Math,
 <<70, 79, 82, 49, 0, 0, 4, 200, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 157,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:sum, 2}}
iex(2)> Math.sum(1, 2)
3

モジュールの関数を呼び出すときは「モジュール名.関数名」だな。

コンパイル

モジュールをファイルに書いて保存しておき、コンパイルすると再利用しやすくなる。例えば次のようなファイルを作る。拡張子は ex。

defmodule Math do

  def sum(a, b) do
    a + b
  end

end

このソースファイルは elixirc コマンドで Erlang VM のバイトコードにコンパイルできる。

^o^ > elixirc math.ex

^o^ > ls
Elixir.Math.beam  hello.exs  math.ex

Elixir.Math.beam っていうファイルが、コンパイルされたバイトコード。ここで iex を立ち上げると、Math モジュールが使えるようになる。カレントディレクトリのバイトコードは自動的に読み込まれるってわけだ。

^o^ > iex
Eshell V8.0  (abort with ^G)
Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Math.sum(1, 2)
3

スクリプトモード

コンパイルを必要としないのがスクリプトモード。こないだの記事で Hello, world プログラムを作ったのがこれ。こっちは拡張子を exs にする。ex ファイルと exs ファイルは中身的には区別はなく、違うのは目的。ex はコンパイルされるのを意図しているのに対して、exs はコンパイルせずにスクリプトモードで使用する。

defmodule Math do

  def sum(a, b) do
    a + b
  end

end

IO.puts Math.sum(1, 2)

上のスクリプト math.exs を実行すると次のように出力される。おっと、さっき作った Elixir.Math.beam を消しておくのを忘れないように。そうでないと「Math モジュールを再定義しようとしてる」とかいう意味(たぶん)の warning が出る。

^o^ > rm Elixir.Math.beam

^o^ > elixir math.exs
3

Elixir スクリプトの実行は elixir コマンドね。

名前付き関数

モジュールの中で、def/2 を使って関数を定義するのは上でもやった。この関数はモジュールの外からも呼べる関数になる。一方、defp/2 を使って定義するとモジュールの外からは呼べないプライベート関数になる。これらは名前付き関数と呼ばれる、たぶん、文脈からすると。

defmodule Math do

  def sum(a, b) do
    a + b
  end

  defp do_sum(a, b) do
    a + b
  end

end

IO.puts Math.sum(1, 2)
IO.puts Math.do_sum(1, 2)

上のスクリプトでは、関数 sum とプライベート関数 do_sum を定義して、呼び出している。これを実行すると:

^o^ > elixir math2.exs
warning: function do_sum/2 is unused
  math2.exs:6

3
** (UndefinedFunctionError) function Math.do_sum/2 is undefined or private
    Math.do_sum(1, 2)
    math2.exs:13: (file)
    (elixir) lib/code.ex:363: Code.require_file/2

こうなる。
Elixir の関数宣言(宣言て書いてあるな)は、ガードと複数句に対応している。例えばこんなふうに:

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)
IO.puts Math.zero?(1)
^o^ > elixir math3.exs
true
false

関数のキャプチャ

関数を値として取り出すことをキャプチャと言い、& を頭につける。math.exs を iex で使ってみよう。

^o^ > iex math.exs
Eshell V8.0  (abort with ^G)
3
Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> fun = &Math.sum/2
&Math.sum/2
iex(2)> fun.(1, 3)
4

キャプチャ構文は関数を作るときのショートカットとしても使える。この場合 &1 は1番目の引数を意味する。

iex(3)> fun = &(&1 + 1)
#Function<6.52032458/1 in :erl_eval.expr/5>
iex(4)> fun.(2)
3

デフォルト引数

名前付き関数はデフォルト値を持つ引数をとることができる。

defmodule Concat do

  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end

end

IO.puts Concat.join("hello", "world")
IO.puts Concat.join("hello", "world", "-")
^o^ > elixir concat.exs
hello world
hello-world

キーワードリスト,マップ,ディクショナリ

 cf. 7 キーワード,マップそしてリスト – Keywords, maps and dicts – Elixir

Elixir には、キーワードリストとマップという2つの連想データ構造がある。

キーワードリスト

2要素のタプルのリストのうち、タプルの1つ目の要素(つまりキー)がアトムであるような構造を Elixir ではキーワードリストと呼んでいる。

iex(1)> list = [{:a, 1,}, {:b, 2}]
[a: 1, b: 2]

返り値にあるように、キーワードリストには [a: 1, b: 2] のような記法がある。

iex(2)> list == [a: 1, b: 2]
true

要素にアクセスするには [] にキーを指定する。

iex(3)> list[:a]
1

キーワードリストはあくまでもリストなので、例えば ++ で連結できる。

iex(4)> [a: 0] ++ list
[a: 0, a: 1, b: 2]

上のように、キーが重複しても構わない。もっともこれの何がうれしいのか。
キーが複数あるとき、値を得ようとすると前にある値を返すことに注意。

iex(5)> list = [a: 0, a: 1, b: 2]
[a: 0, a: 1, b: 2]
iex(6)> list[:a]
0

キーワードリストの重要な特徴は2つ:

  • キーの順序を開発者が指定したとおりに保持する
  • キーを複数回与えることができる

この特徴のため、キーワードリストは関数へオプションを渡すためのデフォルトのメカニズムになっている。例えば、if/2 はこんな書き方もできる。

iex(7)> if(false, [do: :this, else: :that])
:that

一般に、キーワードリストは引数の最後にあるので、角カッコはつけなくてもいい。

マップ

マップは Ruby でいうところの Hash だ。

iex(8)> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex(9)> map[:a]
1
iex(10)> map[2]
:b

マップの特徴は:

  • キーをどんな値にもできる
  • キーは順番通りにならない

マップのキーがすべてアトムの場合には、キーワードリストと似たような構文(キーワード構文)を使うことができる。

iex(11)> map =%{a: 1, b: 2}
%{a: 1, b: 2}

パターンマッチングもできる。

iex(12)> %{} = %{a: 1, b: 2}
%{a: 1, b: 2}
iex(13)> %{a: a} = %{a: 1, b: 2}
%{a: 1, b: 2}
iex(14)> a
1
iex(15)> %{c: c} = %{a: 1, b: 2}
** (MatchError) no match of right hand side value: %{a: 1, b: 2}

マップのパターンマッチングは、部分的にマッチする。だからからのマップはすべてのマップにマッチする一方、存在しないキーにマッチさせようとするとエラーになる。
アトムのキーにはアクセスするための特別の構文が用意されている。

iex(15)> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex(16)> map.a
1

更新のための構文もある。

iex(17)> %{map | :a => 2}
%{2 => :b, :a => 2}
iex(18)> %{map | :c => 3}
** (KeyError) key :c not found in: %{2 => :b, :a => 1}
    (stdlib) :maps.update(:c, 3, %{2 => :b, :a => 1})
    (stdlib) erl_eval.erl:255: anonymous fn/2 in :erl_eval.expr/5
    (stdlib) lists.erl:1263: :lists.foldl/3

更新するキーは存在するキーでなければいけない。存在しないキーに対して更新しようとするとエラーになる。

ディクショナリ

ディクショナリはインターフェイスのようなもの(Elixir ではビヘイビアと呼ぶ)で、キーワードリストもマップもこのインターフェイスを実装している。

iex(18)> keyword = []
[]
iex(19)> map = %{}
%{}
iex(20)> Dict.put(keyword, :a, 1)
[a: 1]
iex(21)> Dict.put(map, :a, 1)
%{a: 1}

このように、Dict モジュールを使うとキーワードリストとマップを同じように扱える。

バイナリ,文字列,文字リスト

 cf. 6 バイナリ,文字列そして文字リスト – Binaries, strings and char lists – Elixir

バイナリ

バイナリは Elixir のデータの一つでビット列……もっと正確に言うとバイト列だ。バイナリは << >> で表すことができる。

iex(1)> x = <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex(2)> is_binary x
true

バイナリが何バイトあるかは byte_size/1 で求められる。

iex(3)> byte_size x
4

バイナリの連結は <> 演算子。文字列の連結演算子と同じだけど、その理由は後から出てくる。

iex(4)> <<1, 2>> <> <<3, 4>>
<<1, 2, 3, 4>>

文字列

Elixir において、文字列は特殊なバイナリだ。

iex(5)> is_binary "hello"
true

特殊、とはどういうことかというと、そのバイナリが UTF-8 で解釈できるか(エンコードできるか)ということだ。だから次のような例では、バイナリを作っても文字列が返ってくる。

iex(6)> <<104, 101, 108, 108, 111>>
"hello"

逆に文字列のバイナリ表現を見るには、ヌルバイト <<0>> を連結してみるのが一般的らしい。

iex(7)> "hello" <> <<0>>
<<104, 101, 108, 108, 111, 0>>

パターンマッチング

バイナリもパターンマッチングできる。バイナリのパターンはちょうど1バイト(8ビット)ずつを期待していることに注意。

iex(8)> <<1, x, 3>> = <<1, 2, 3>>
<<1, 2, 3>>
iex(9)> x
2
iex(10)> <<1, 2, x :: binary>> = <<1, 2, 3, 4>>
<<1, 2, 3, 4>>
iex(11)> x
<<3, 4>>

2番目の例では、x に「残り」のバイナリが束縛される。

文字リスト

"(二重引用符)は文字列を作る。これに対して '(単一引用符)は文字リストを作る。

iex(12)> 'hello'
'hello'
iex(13)> is_list 'hello'
true

文字列と文字リストは別のもの。

iex(14)> 'hello' == "hello"
false

文字リストから文字列に変換するには to_string/1 が使える。

iex(15)> to_string 'hello'
"hello"

補足

このエントリでは、関数呼び出しをカッコなしで書いた: is_binary x のように。Elixir では関数呼び出しの引数を囲むカッコは必須ではない。