概要
名前付き関数に関して解説している章。
defmodule Calculator do
def add_one(n) do
n + 1
end
end
上記の様に名前付き関数はモジュール内に定義する。
この関数は一つの引数を取り、add_one/1
という名前の関数である。
モジュールのコンパイル
iex に対して 2 通りの自作のモジュールをコンパイルする方法がある。
一つ目は iex を立ち上げる際に読み込む方法
$ iex calculator.exs
二つ目が iex 上で c ヘルパーで読み込む方法
iex> c "calculator.exs"
[Calculator]
iex> Calculator.add_one(2)
3
iex> Calculator.add_one("2")
** (ArithmeticError) bad argument in arithmetic expression
calculator.exs:3: Calculator.add_one/1
上記の様にコンパイル後は自由に使うことができ、問題がある場合は例外を発生させて教えてくれる。
また、エラーでは add_one/1
と表示されているのがわがるが、Elixir では名前付き関数を名前とアリティ (arity, パラメーター数) で識別する。
関数のボディの記述方法
概要では do...end
ブロックを使い、複数式をグループ化して記述しているが、do...end
は基本的な構文ではなく、実際の構文は以下のようになる。
def add_one(n), do: n + 1
do:
で複数行を記述する場合は ()
で囲む
def greet(greeting, name), do: (
IO.puts greeting
IO.puts "How're you doing, #{name}?"
)
do...end
はただのシンタックスシュガーで、コンパイル時に do:
形式に変換される。
ただ、複数行に渡るときは通常 do...end
を使う。
パターンマッチ
5 章で Elixir のパターンマッチによる強力な関数呼び出し方法を学んだが、名前付き関数でも同じように使うことができる。
違いは、名前付き関数の場合は毎回関数名の宣言をする必要があることだが、これによって関数をとても数学の定義近い形で表現することができる。
例えば階乗の定義は
factorial(0) -> 1
factorial(n) -> n * factorial(n - 1)
となるが、Elixir だと以下のように実装する
defmodule Factorial do
def of(0), do: 1
def of(n), do: n * of(n - 1)
end
0 が渡されるまで、パターンマッチで of(n)
が呼び出され続けて、0 が渡されたタイミングで of(0)
がマッチして 1 が返され、再帰が終了する。
iex> Factorial.of(3)
6
iex> Factorial.of(7)
5040
iex> Factorial.of(10)
3628800
このように、パターンマッチの特性により、とても自然に実装することができる。
この際、注意が必要なのはパターンマッチはコードの上から下に行われることである。そのため、以下の様に記述すると動かない。
defmodule Factorial do
def of(n), do: n * of(n - 1)
def of(0), do: 1
end
このようなミスを無くすために、Elixir はコンパイル時に以下のような warning を出してくれる。
iex> c "factorial.exs"
warning: this clause cannot match because a previous clause at line 2 always matches
factorial.exs:3
ガード節
パターンマッチの力は前回の無名関数でも見てきたが、動作を値の型や条件で区別したい場合にはマッチングだけでは不十分である。その際に使うのが when
キーワードを使ったガード節である。
defmodule Guard do
def what_is(x) when is_number(x) do
IO.puts "#{x} is a number"
end
def what_is(x) when is_list(x) do
IO.puts "#{inspect(x)} is a list"
end
def what_is(x) when is_atom(x) do
IO.puts "#{x} is a atom"
end
end
iex> Guard.what_is(99)
99 is a number
:ok
iex> Guard.what_is(:cat)
cat is a atom
:ok
iex> Guard.what_is([1, 2, 3])
[1, 2, 3] is a list
:ok
上記の階乗の実装を見直すと、負の値を渡したときに正しく動作しないことがわかる。
そこでガード節を使ってこの問題を解消する。
defmodule Factorial do
def of(0), do: 1
def of(n) when n > 0 do
n * of(n - 1)
end
end
iex> Factorial.of(-10)
** (FunctionClauseError) no function clause matching in Factorial.of/1
iex:6: Factorial.of(-10)
ガード節で使える演算子はリファレンスにまとまっている
デフォルトパラメータ
デフォルトパラメータは (パラメータ) \\ (値)
という形式で書く。
defmodule DefaultParameter do
def test(p1, p2 \\ 2, p3 \\ 3, p4) do
IO.puts "p1: #{p1}, p2: #{p2}, p3: #{p3}, p4: #{p4}"
end
end
関数に渡された値は左のパラメータから束縛されていき、少ない分はデフォルトの値が使われる。
必須パラメータよりも引数が少なければエラーとなる。
iex> DefaultParameter.test("a", "b")
p1: a, p2: 2, p3: 3, p4: b
iex> DefaultParameter.test("a", "b", "c")
p1: a, p2: b, p3: 3, p4: c
iex> DefaultParameter.test("a", "b", "c", "d")
p1: a, p2: b, p3: c, p4: d
iex> DefaultParameter.test("a")
** (UndefinedFunctionError) function DefaultParameter.test/1 is undefined or private. Did you mean one of:
* test/2
* test/3
* test/4
DefaultParameter.test("a")
以下のような関数は、コンパイルするとエラーになる
defmodule DefaultParameter do
def test(p1, p2 \\ 2, p3 \\ 3, p4) do
IO.puts "p1: #{p1}, p2: #{p2}, p3: #{p3}, p4: #{p4}"
end
def test(p1, p2) do
IO.puts "p1: #{p1}, p2: #{p2}"
end
end
** (CompileError) iex:18: def test/2 conflicts with defaults from def test/4
最初の関数定義が引数 2, 3, 4 の場合、全てにマッチしてしまうためである。
複数のボディを持つ関数にデフォルト値を与える場合は、以下の様にボディなしのデフォルトパラメータを含む関数を最初に定義して、それ以降にボディの定義をしていく。
defmodule DefaultParameter do
def test(p1, p2 \\ :default)
def test(p1, p2) when is_list(p1) do
IO.puts "p1 is list, p2: #{p2}"
end
def test(p1, p2) do
IO.puts "p1: #{p1}, p2: #{p2}"
end
end
iex> DefaultParameter.test("a")
p1: a, p2: default
iex> DefaultParameter.test("a", "b")
p1: a, p2: b
iex> DefaultParameter.test(["a"], "b")
p1 is list, p2: b
プライベート関数
プライベート関数は defp
キーワードで定義する。プライベート関数は、定義されたモジュール内でしか呼び出せない。
defmodule PrivateSample do
def func do
IO.puts pfunc
end
defp pfunc do
"private function"
end
end
iex> PrivateSample.func
private function
iex> PrivateSample.pfunc
** (UndefinedFunctionError) function PrivateSample.pfunc/0 is undefined or private. Did you mean one of:
* func/0
PrivateSample.pfunc()
通常の関数と同様に複数のヘッドを持たせることができるが、あるヘッドをプライベートに、他をパブリックにということはできない。
defmodule PrivateSample do
def pfunc(0) do
IO.puts 0
end
defp pfunc(n) do
IO.puts n
end
end
== Compilation error on file sample.exs ==
** (CompileError) sample.exs:6: defp pfunc/1 already defined as def
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
パイプ演算子
例えば他のプログラミング言語だと、以下のようなプログラムをよく書く
people = DB.find_customers
orders = Orders.for_customers(people)
tax = sales_tax(orders, 2016)
filing = prepare_filing(tax)
これを「一度しか使わないのに変数に入れるのは嫌だ」となると以下のようなプログラムになる。
prepare_filing(sales_tax(Orders.for_customers(DB.find_customers), 2016))
流石に、読みづらい上に、実行する順番が直観的でなく、難しい。
しかし、Elixir ならパイプ演算子 |>
を使って、これを簡潔に書ける。
filing = DB.find_customers
|> Orders.for_customers
|> sales_tax(2016)
|> prepare_filing
左の式の結果を、右の式の第一引数として渡して実行する。
つまり、val |> f(a, b)
は f(val, a, b)
と同じ意味である。
上記の様に改行せずに一行で書くこともできる。
iex> (1..5) |> Enum.map(&(&1 + 1)) |> Enum.map(&(&1 * 2))
[4, 6, 8, 10, 12]
ただし、パイプ演算子を使って、パラメータ付きで関数を呼ぶ場合は括弧を使う。
このように、パイプ演算子を使えば、「これをやって、これをやって、、、」と思考の流れと近い形でプログラミングをすることができる。
とても強力な機能である。
モジュール
定義するものに、ネームスペースを提供する。
defmodule OuterModule do
defmodule InnerModule do
def func1 do
IO.puts "func1 in InnerModule"
end
def func2 do
func1
IO.puts "func2 in InnerModule"
end
end
def func1 do
InnerModule.func1
IO.puts "func1 in OuterModule"
end
end
iex> OuterModule.func1
func1 in InnerModule
func1 in OuterModule
iex> OuterModule.InnerModule.func1
func1 in InnerModule
iex> OuterModule.InnerModule.func2
func1 in InnerModule
func2 in InnerModule
同じネームスペース内では、モジュール名を省略して呼び出すことができる。
モジュールは全てトップレベルで定義され、モジュール内にモジュールを定義すると Elixir はドットでつなげて定義する。
そのため、以下の様に定義することもできる。
defmodule OuterModule.InnerModule do
def func3, do: "func3 in InnerModule"
end
iex> OuterModule.InnerModule.func3
"func3 in InnerModule"
ディレクティブ
モジュールには三種類のディレクティブが用意されている。
import
import Module [, only: | except: ]
モジュールの関数やマクロをカレントスコープに持ってくる。
例えば以下の例は String の split/2 関数を import している。
defmodule ImportSample do
def split_by_space1(str) do
String.split(str, " ")
end
def split_by_space2(str) do
import String, only: [split: 2]
split(str, " ")
end
end
iex> ImportSample.split_by_space1("hoge fuga")
["hoge", "fuga"]
iex> ImportSample.split_by_space2("hoge fuga")
["hoge", "fuga"]
import してくることで、モジュール名を省略することができる。
only
, except
オプションは function_name: arity
を渡すことで、import する関数を絞ることができる。また、:function
, :macros
を渡して、関数かマクロかを絞ることもできる。
alias
alias Module [, as:]
モジュールのエイリアスを作成して、モジュール名を省略することができる
defmodule Too.Long.Name.SampleModule do
def func1 do
"alias sample"
end
end
defmodule AliasModule do
def func1 do
alias Too.Long.Name.SampleModule, as: SampleModule
SampleModule.func1()
end
end
SampleModule.func1()
だけで呼び出せるようになるのがわかる。
また、デフォルトでモジュール名の最後の値をエイリアスにするので、上記のような場合 as:
を省略できる。
alias Too.Long.Name.SampleModule
また、複数ある場合はタプルで渡せる。
alias Too.Long.Name.{SampleModule1, SampleModule2, SampleModule3}
require
require Module
モジュールで定義したマクロが使えるようになる。
属性
各モジュールはメタデータを持っており、それぞれをモジュールの属性という。
設定やメタデータとして使われる。
属性名には @
がついており、以下のように値をセットする。
@name value
値のセットはモジュールのトップレベルのみ可能で、関数からはアクセスのみ可能。
defmodule AttributeSample do
@name "Yamada Taro"
def user_name do
@name
end
end
iex> AttributeSample.user_name
"Yamada Taro"
関数内で値をセットするとエラーになる。
iex> defmodule AttributeSample do
...> @name "Yamada Taro"
...>
...> def set_user_name do
...> @name "Yamada Jiro"
...> end
...> end
warning: redefining module AttributeSample (current version defined in memory)
iex:49
** (ArgumentError) cannot set attribute @name inside function/macro
(elixir) lib/kernel.ex:2336: Kernel.do_at/4
(elixir) expanding macro: Kernel.@/1
iex:53: AttributeSample.set_user_name/0
値のセットは何度でも可能で、関数内で属性アクセスすると直前でセットされた値が使われる。
defmodule AttributeSample do
@name "Yamada Taro"
def user_name1 do
@name
end
@name "Yamada Jiro"
def user_name2 do
@name
end
end
iex> AttributeSample.user_name1
"Yamada Taro"
iex> AttributeSample.user_name2
"Yamada Jiro"
モジュールの名前について
Elixir は String.length("hoge")
のように関数を呼び出すが、内部ではモジュール名をアトムに変換している。
iex> to_string String
"Elixir.String"
iex> :"Elixir.String"
String
そのため以下の様に呼び出すこともできる。
iex> :"Elixir.String".length("hoge")
4
Erlang の関数を呼び出す
Erlang は Elixir と違い、変数は大文字から始まり、アトムは小文字で命名される。そのため、Erlang の timer モジュールを使いたければ、timer をアトムにして呼べば良い。
iex> :timer.minutes(1)
60000