LoginSignup
4
4

More than 5 years have passed since last update.

ElixirのGETTING STARTED(8.Modules)をやってみた

Last updated at Posted at 2015-07-19

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で、次の内容内容を持っているとする。

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 というファイルを作成することができる。

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

もしデフォルト値を使うなら、関数定義の上書きについては十分な注意が必要。
次のような例がある。

concat.ex
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"

モジュールの簡単な導入はおしまい。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4