Elixir入門 08: モジュールと関数
gumi TECH
Posted on November 6, 2018
本稿はElixir公式サイトの許諾を得て「Modules and functions」の解説にもとづき、加筆補正を加えてElixirのモジュールと関数についてご説明します。
Elixirはモジュールに関数をまとめてグループ化します。たとえば、String.length/1
はUTF-8のUnicode文字数を調べるString
モジュールの関数です。
iex> String.length("hello")
5
モジュールをつくるには、defmodule/2
マクロを用います。さらに、その中に関数を定めるのがdef/2
マクロです。モジュールと関数はつぎの構文でつくられます。
iex> defmodule Math do
...> def sum(a, b) do
...> a + b
...> end
...> end
iex> Math.sum(1, 2)
3
モジュールや関数を定めてコードが長くなると、iex
モードではテストしにくくなるでしょう。コンパイルやスクリプトモードを使えば、ファイルに書いたプログラムが試せます。
コンパイル
Elixirのプログラムを書いたファイルには拡張子.ex
をつけます。たとえば、つぎのコードをmath.ex
というファイル名で保存したとしましょう。
defmodule Math do
def sum(a, b) do
a + b
end
end
コンパイルするには、コマンドラインツールからelixirc ファイル名
を打ち込んでください。コンパイルされたバイトコードのファイルがElixir.モジュール名.beam
という名前でつくられるはずです(図001)。
$ elixirc math.ex
図001■モジュールのバイトコードがつくられた
そのあとiex
を起ち上げれば、そのディレクトリにあるバイトコードファイル(.beam
)が読み込まれ、モジュールも使えるようになるのです。
iex> Math.sum(1, 2)
3
実際の開発にはmix
というビルドツールを使うことになるでしょう。プロジェクトの作成、コンパイル、テスト、依存関係の管理などがこのツールの役割です。Elixirのプロジェクトでは、通常つぎの3つのディレクトリにそれぞれのファイルを納めて開発します。
- ebin - コンパイルされたバイトコードファイル
- lib - Elixirコードファイル(拡張子
.ex
) - test - テストファイル(拡張子
.exs
)
スクリプトモード
スクリプトモードではバイトコードファイルはつくることなく、プログラムを実行します。ファイルにつける拡張子は.exs
です。中身は.ex
ファイルと同じで、コンパイルしたモジュールがメモリに読み込まれて実行されます。バイトコードをディスクに書き出さないことを示すため、異なる拡張子が使われるのです。
defmodule Math do
def sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2)
スクリプトモードで実行するには、コマンドラインツールからコマンドelixir ファイル名
入力します。プログラムから結果を出力するには。IO.puts/2
関数をお使いください。
$ elixir math.exs
3
モジュールを読み込んだうえでiex
で使いたいときは、コマンドラインツールからiex ファイル名
のコマンドで起ち上げてください。
$ iex math.exs
iex> Math.sum(3, 4)
7
名前つき関数
モジュールにdef/2
で定めた関数は、他のモジュールから呼び出せます。外から参照させないプライベートな関数を定義するために用いるのがdefp/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)
$ elixir math.exs
3
** (UndefinedFunctionError) function Math.do_sum/2 is undefined or private
Math.do_sum(1, 2)
math.exs:12: (file)
(elixir) lib/code.ex:677: Code.require_file/2
関数の定めには、ガードと複数の句が加えられます。複数の句は、Elixirが上から順に試し、マッチした句を実行するのです。引数がいずれにもマッチしなければ、エラーになります。なお、関数名の最後に疑問符?
を添えるのは、論理値が返される場合のElixirにおける命名規則です。
defmodule Math do
def zero?(0) do
true
end
def zero?(x) when is_integer(x) do
false
end
end
IO.puts Math.zero?(0) #=> true
IO.puts Math.zero?(1) #=> false
IO.puts Math.zero?([1]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0) #=> ** (FunctionClauseError)
if/2
と同じく、名前つき関数にはdo/end
ブロックのほか、do:
を用いたキーワードリストの構文が使えます(「Elixir入門 05: 条件 - case/cond/if」「do/endブロック」および「Elixir入門 07: キーワードリストとマップ」「キーワードリスト」参照)。前掲コードは、つぎのようにも書けるのです。
defmodule Math do
def zero?(0), do: true
def zero?(x) when is_integer(x), do: false
end
ただし、do:
の構文は1行で済む場合に用い、複数行にわたるときはdo/end
ブロックで書いた方がよいでしょう。
関数のキャプチャ
前掲のMath.zero?/1
が定められたモジュール(ファイル名math.exsとします)を例に、iex
で関数のキャプチャを試してみましょう。
$ iex math.exs
iex> Math.zero?(0)
true
Elixirは無名関数と名前つき関数を区別します。無名関数の納められた変数から関数を呼び出すには、変数のあとにドット.
を添えなければなりません(「Elixir入門 02: 型の基本」「無名関数」参照)。
キャプチャ演算子&/1
を用いると、名前つき関数を変数に入れて、無名関数と同じように呼び出せます。代入する関数には、アリティを添えてください。なお、is_function/1
は引数が関数かどうかを確かめます。
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function(fun)
true
iex> fun.(0)
true
組み込み済みの関数も、&/1
演算子で変数に納めて呼び出せます。
iex> is_fun = &(is_function/1)
&:erlang.is_function/1
iex> is_fun.(fun)
true
iex> (&is_number/1).(1.0)
true
&/1
演算子を用いると無名関数も簡略に書けます。たとえば、つぎの無名関数を定めたいとしましょう。
square = fn(x) -> x * x end
&/1
演算子を使えば、つぎのように短く書けます。さらに、他の無名関数とも組み合わせられるのです。
iex> square = &(&1 * &1)
#Function<6.99386804/1 in :erl_eval.expr/5>
iex> square.(2)
4
iex> square_sum = &(square.(&1) + square.(&2))
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> square_sum.(3, 4)
25
モジュールの関数もキャプチャすると、モジュールなしで呼び出せるようになります。List.flatten/2
は引数のふたつのリストをつなげたうえで、入れ子を平坦化する関数です。引数ふたつを与えていますので、キャプチャにアリティは添えません。
iex> flatten = &List.flatten(&1, &2)
&List.flatten/2
iex> flatten.([1, [[2], 3]], [4, 5])
[1, 2, 3, 4, 5]
モジュールの関数を利用した新たな関数もつくれます。なお、math
はErlangのモジュールで、:math.sqrt/1
は平方根を求める関数です。
iex> hypot = &:math.sqrt(square_sum.(&1, &2))
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> hypot.(3, 4)
5.0
上の関数はつぎのようにも定められます。
iex> hypot = &:math.sqrt(&1 * &1 + &2 * &2)
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> hypot.(3, 4)
5.0
デフォルト引数
名前つき関数の引数には、あとに\\
に続けてデフォルト値が定められます。
defmodule DefaultTest do
def dowork(x \\ "hello") do
x
end
end
iex> DefaultTest.dowork()
"hello"
iex> DefaultTest.dowork("hi")
"hi"
iex> DefaultTest.dowork(1)
1
デフォルト値にはどのような式でも与えられます。ただし、値が評価されるのは、関数を定義したときではありません。関数が呼び出されてデフォルト値が用いられるたびに、その値は評価されるのです。
defmodule Concat do
def join(a, b, sep \\ " ") do
a <> sep <> b
end
end
iex(2)> Concat.join("hello", "world")
"hello world"
iex(3)> Concat.join("hello", "world", ", ")
"hello, world"
複数の句をもつ関数にも、デフォルト値は使えます。ただし、アリティを同じくする句には、関数本体のないヘッダで定めなければなりません。コンパイルエラーのメッセージにはその旨が示されます。
defmodule Greeter do
def hello(name \\ nil, language \\ "em") when is_nil(name) do
phrase(language) <> "world"
end
def hello(name, language \\ "en") do
phrase(language) <> name
end
defp phrase("en"), do: "hello, "
defp phrase("ja"), do: "こんにちは"
end
** (CompileError) greeter.ex:6: definitions with multiple clauses and default values requi
re a header. Instead of:
def foo(:first_clause, b \\ :default) do ... end
def foo(:second_clause, b) do ... end
one should write:
def foo(a, b \\ :default)
def foo(:first_clause, b) do ... end
def foo(:second_clause, b) do ... end
def hello/2 has multiple clauses and defines defaults in one or more clauses
greeter.ex:6: (module)
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
句が複数ある関数のデフォルト値は、ヘッダの引数に\\
で与えてください。
defmodule Greeter do
def hello(name \\ nil, language \\ "en")
def hello(name, language) when is_nil(name) do
phrase(language) <> "world"
end
def hello(name, language) do
phrase(language) <> name
end
defp phrase("en"), do: "hello, "
defp phrase("ja"), do: "こんにちは"
end
iex> Greeter.hello()
"hello, world"
iex> Greeter.hello("alice")
"hello, alice"
iex> Greeter.hello("太郎", "ja")
"こんにちは太郎"
デフォルト値を与えるときは、関数の定義が重複しないよう気をつけてください。
defmodule Concat do
def join(a, b) do
IO.puts "#=> join/2"
a <> b
end
def join(a, b, sep \\ " ") do
IO.puts "#=> join/3"
a <> sep <> b
end
end
上の関数をコンパイルすると、つぎのような警告が示されます。ふたつの引数を渡すとつねにアリティ2の関数が呼び出され、アリティ3のデフォルト値が使われることはないからです。
warning: this clause cannot match because a previous clause at line 2 always matches
エラーではありませんので、コンパイルはでき、関数も呼び出せます。アリティ3の関数は3つの引数を渡さないかぎり呼び出されません。引数がふたつのときどういう結果を得たいのか考え直すべきでしょう。
iex(1)> Concat.join("hello", "world")
#=> join/2
"helloworld"
iex(2)> Concat.join("hello", "world", ", ")
#=> join/3
"hello, world"
つぎのコードは、関数のデフォルト値とパターンマッチングを使った例です。Enum.join/2
は、リスト(Enumerable
)要素の間に第2引数の文字列を挟んで、バイナリ(文字列)につなげる関数です。
defmodule Greeter do
def hello(names, language \\ "en")
def hello(names, language) when is_list(names) do
hello(Enum.join(names, ", "), language)
end
def hello(name, language) when is_binary(name) do
phrase(language) <> name
end
defp phrase("en"), do: "hello, "
defp phrase("ja"), do: "こんにちは"
end
iex> Greeter.hello("alice")
"hello, alice"
iex> Greeter.hello(["alice", "carroll"])
"hello, alice, carroll"
iex> Greeter.hello(["桃太郎", "金太郎", "浦島太郎"], "ja")
"こんにちは桃太郎, 金太郎, 浦島太郎"
なお、上のコードの最初の関数は、パイプ演算子|>
を用いると、つぎのようにすっきりと書き替えられます。|>
は左オペランドの値を、右オペランドの関数に第1引数として渡す演算子です。
def hello(names, language) when is_list(names) do
# hello(Enum.join(names, ", "), language)
names
|> Enum.join(", ")
|> hello(language)
end
Elixir入門もくじ
- Elixir入門 01: コードを書いて試してみる
- Elixir入門 02: 型の基本
- Elixir入門 03: 演算子の基本
- Elixir入門 04: パターンマッチング
- Elixir入門 05: 条件 - case/cond/if
- Elixir入門 06: バイナリと文字列および文字リスト
- Elixir入門 07: キーワードリストとマップ
- Elixir入門 08: モジュールと関数
- Elixir入門 09: 再帰
- Elixir入門 10: EnumとStream
- Elixir入門 11: プロセス
- Elixir入門 12: 入出力とファイルシステム
- Elixir入門 13: aliasとrequireおよびimport
- Elixir入門 14: モジュールの属性
- Elixir入門 15: 構造体
- Elixir入門 16: プロトコル
- Elixir入門 17: 内包表記
- Elixir入門 18: シギル
- Elixir入門 19: tryとcatchおよびrescue
- Elixir入門 20: 型の仕様とビヘイビア
- Elixir入門 21: デバッグ
- Elixir入門 22: Erlangライブラリ
- Elixir入門 23: つぎのステップ
番外
Posted on November 6, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.