Elixir入門 08: モジュールと関数

gumitech

gumi TECH

Posted on November 6, 2018

Elixir入門 08: モジュールと関数

本稿はElixir公式サイトの許諾を得て「Modules and functions」の解説にもとづき、加筆補正を加えてElixirのモジュールと関数についてご説明します。

Elixirはモジュールに関数をまとめてグループ化します。たとえば、String.length/1はUTF-8のUnicode文字数を調べるStringモジュールの関数です。

iex> String.length("hello")
5
Enter fullscreen mode Exit fullscreen mode

モジュールをつくるには、defmodule/2マクロを用います。さらに、その中に関数を定めるのがdef/2マクロです。モジュールと関数はつぎの構文でつくられます。

iex> defmodule Math do
...>   def sum(a, b) do
...>     a + b
...>   end
...> end

iex> Math.sum(1, 2)
3
Enter fullscreen mode Exit fullscreen mode

モジュールや関数を定めてコードが長くなると、iexモードではテストしにくくなるでしょう。コンパイルやスクリプトモードを使えば、ファイルに書いたプログラムが試せます。

コンパイル

Elixirのプログラムを書いたファイルには拡張子.exをつけます。たとえば、つぎのコードをmath.exというファイル名で保存したとしましょう。

defmodule Math do
  def sum(a, b) do
    a + b
  end
end
Enter fullscreen mode Exit fullscreen mode

コンパイルするには、コマンドラインツールからelixirc ファイル名を打ち込んでください。コンパイルされたバイトコードのファイルがElixir.モジュール名.beamという名前でつくられるはずです(図001)。

$ elixirc math.ex
Enter fullscreen mode Exit fullscreen mode

図001■モジュールのバイトコードがつくられた

elixir_08_001.png

そのあとiexを起ち上げれば、そのディレクトリにあるバイトコードファイル(.beam)が読み込まれ、モジュールも使えるようになるのです。

iex> Math.sum(1, 2)
3
Enter fullscreen mode Exit fullscreen mode

実際の開発には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)
Enter fullscreen mode Exit fullscreen mode

スクリプトモードで実行するには、コマンドラインツールからコマンドelixir ファイル名入力します。プログラムから結果を出力するには。IO.puts/2関数をお使いください。

$ elixir math.exs
3
Enter fullscreen mode Exit fullscreen mode

モジュールを読み込んだうえでiexで使いたいときは、コマンドラインツールからiex ファイル名のコマンドで起ち上げてください。

$ iex math.exs
Enter fullscreen mode Exit fullscreen mode
iex> Math.sum(3, 4)
7
Enter fullscreen mode Exit fullscreen mode

名前つき関数

モジュールに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)
Enter fullscreen mode Exit fullscreen mode
$ 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
Enter fullscreen mode Exit fullscreen mode

関数の定めには、ガードと複数の句が加えられます。複数の句は、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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

ただし、do:の構文は1行で済む場合に用い、複数行にわたるときはdo/endブロックで書いた方がよいでしょう。

関数のキャプチャ

前掲のMath.zero?/1が定められたモジュール(ファイル名math.exsとします)を例に、iexで関数のキャプチャを試してみましょう。

$ iex math.exs
Enter fullscreen mode Exit fullscreen mode
iex> Math.zero?(0)
true
Enter fullscreen mode Exit fullscreen mode

Elixirは無名関数と名前つき関数を区別します。無名関数の納められた変数から関数を呼び出すには、変数のあとにドット.を添えなければなりません(「Elixir入門 02: 型の基本」「無名関数」参照)。

キャプチャ演算子&/1を用いると、名前つき関数を変数に入れて、無名関数と同じように呼び出せます。代入する関数には、アリティを添えてください。なお、is_function/1は引数が関数かどうかを確かめます。

iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function(fun)
true
iex> fun.(0)
true
Enter fullscreen mode Exit fullscreen mode

組み込み済みの関数も、&/1演算子で変数に納めて呼び出せます。

iex> is_fun = &(is_function/1)
&:erlang.is_function/1
iex> is_fun.(fun)
true
iex> (&is_number/1).(1.0)
true
Enter fullscreen mode Exit fullscreen mode

&/1演算子を用いると無名関数も簡略に書けます。たとえば、つぎの無名関数を定めたいとしましょう。

square = fn(x) -> x * x end
Enter fullscreen mode Exit fullscreen mode

&/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
Enter fullscreen mode Exit fullscreen mode

モジュールの関数もキャプチャすると、モジュールなしで呼び出せるようになります。List.flatten/2は引数のふたつのリストをつなげたうえで、入れ子を平坦化する関数です。引数ふたつを与えていますので、キャプチャにアリティは添えません。

iex> flatten = &List.flatten(&1, &2)
&List.flatten/2
iex> flatten.([1, [[2], 3]], [4, 5])
[1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

モジュールの関数を利用した新たな関数もつくれます。なお、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
Enter fullscreen mode Exit fullscreen mode

上の関数はつぎのようにも定められます。

iex> hypot = &:math.sqrt(&1 * &1 + &2 * &2)
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> hypot.(3, 4)
5.0
Enter fullscreen mode Exit fullscreen mode

デフォルト引数

名前つき関数の引数には、あとに\\に続けてデフォルト値が定められます。

defmodule DefaultTest do
  def dowork(x \\ "hello") do
    x
  end
end
Enter fullscreen mode Exit fullscreen mode
iex> DefaultTest.dowork()
"hello"
iex> DefaultTest.dowork("hi")
"hi"
iex> DefaultTest.dowork(1)
1
Enter fullscreen mode Exit fullscreen mode

デフォルト値にはどのような式でも与えられます。ただし、値が評価されるのは、関数を定義したときではありません。関数が呼び出されてデフォルト値が用いられるたびに、その値は評価されるのです。

defmodule Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end
Enter fullscreen mode Exit fullscreen mode
iex(2)> Concat.join("hello", "world")
"hello world"
iex(3)> Concat.join("hello", "world", ", ")
"hello, world"
Enter fullscreen mode Exit fullscreen mode

複数の句をもつ関数にも、デフォルト値は使えます。ただし、アリティを同じくする句には、関数本体のないヘッダで定めなければなりません。コンパイルエラーのメッセージにはその旨が示されます。

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
Enter fullscreen mode Exit fullscreen mode
** (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
Enter fullscreen mode Exit fullscreen mode

句が複数ある関数のデフォルト値は、ヘッダの引数に\\で与えてください。

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
Enter fullscreen mode Exit fullscreen mode
iex> Greeter.hello()
"hello, world"
iex> Greeter.hello("alice")
"hello, alice"
iex> Greeter.hello("太郎", "ja")
"こんにちは太郎"
Enter fullscreen mode Exit fullscreen mode

デフォルト値を与えるときは、関数の定義が重複しないよう気をつけてください。

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
Enter fullscreen mode Exit fullscreen mode

上の関数をコンパイルすると、つぎのような警告が示されます。ふたつの引数を渡すとつねにアリティ2の関数が呼び出され、アリティ3のデフォルト値が使われることはないからです。

warning: this clause cannot match because a previous clause at line 2 always matches
Enter fullscreen mode Exit fullscreen mode

エラーではありませんので、コンパイルはでき、関数も呼び出せます。アリティ3の関数は3つの引数を渡さないかぎり呼び出されません。引数がふたつのときどういう結果を得たいのか考え直すべきでしょう。

iex(1)> Concat.join("hello", "world")
#=> join/2
"helloworld"
iex(2)> Concat.join("hello", "world", ", ")
#=> join/3
"hello, world"
Enter fullscreen mode Exit fullscreen mode

つぎのコードは、関数のデフォルト値とパターンマッチングを使った例です。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
Enter fullscreen mode Exit fullscreen mode
iex> Greeter.hello("alice")
"hello, alice"
iex> Greeter.hello(["alice", "carroll"])
"hello, alice, carroll"
iex> Greeter.hello(["桃太郎", "金太郎", "浦島太郎"], "ja")
"こんにちは桃太郎, 金太郎, 浦島太郎"
Enter fullscreen mode Exit fullscreen mode

なお、上のコードの最初の関数は、パイプ演算子|>を用いると、つぎのようにすっきりと書き替えられます。|>は左オペランドの値を、右オペランドの関数に第1引数として渡す演算子です。

def hello(names, language) when is_list(names) do
  # hello(Enum.join(names, ", "), language)
  names
  |> Enum.join(", ")
  |> hello(language)
end
Enter fullscreen mode Exit fullscreen mode

Elixir入門もくじ

番外
💖 💪 🙅 🚩
gumitech
gumi TECH

Posted on November 6, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related