入出力

 cf. 12 入出力 – IO – Elixir

入出力モジュール

標準入出力を使って出力するには IO.puts、入力するには IO.gets を使う。

iex(1)> IO.puts "hello world"
hello world
:ok
iex(2)> IO.gets "yes or no? "
yes or no? yes
"yes\n"

IO.puts の引数に :stderr を渡すと、標準エラー出力に出力する。

iex(3)> IO.puts :stderr, "hello world"
hello world
:ok

ファイルモジュール

File モジュールを使って、ファイルの読み書きができる。

iex(4)> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.85.0>}
iex(5)> IO.binwrite file, "world"
:ok
iex(6)> File.close file
:ok
iex(7)> File.read "hello"
{:ok, "world"}

ファイルは、デフォルトではバイナリモードで開かれる。:utf8 を指定することで UTF-8 エンコーディングで開くこともできる。
File モジュールに関数には ! (バンと読む)のついた関数とついていない関数がある。違いはエラーを起こすかどうかだ。

iex(8)> File.read "hello"
{:ok, "world"}
iex(9)> File.read! "hello"
"world"
iex(10)> File.read "unknown"
{:error, :enoent}
iex(11)> File.read! "unknown"
** (File.Error) could not read file "unknown": no such file or directory
    (elixir) lib/file.ex:244: File.read!/1

存在するファイル hello を読み込むときにはどちらも同じだけど、存在しないファイル unknown を読み込もうとしたとき、File.read は単に {:error, :enoent} というタプルを返しているだけなのに対して、File.read! はエラーを発生させている。

パスモジュール

Path モジュールは、パス文字列の操作に使う。

iex(11)> Path.join("foo", "bar")
"foo/bar"
iex(12)> Path.expand("hello")
"c:/Users/takatoh/Documents/w/learning-elixir/GettingStarted/hello"

デーモンとして動かすSinatraアプリにユーザーのインストールしたRubyを使わせる

以前作った Sinatra アプリ(フォトビューワー)をデーモンとして動かそうとしてハマった記録。
Sinatra アプリをデーモン化したことは前にもやったことがあるので、基本的にはその時のエントリを見ながらやったんだけど、一つだけ、ハマりどころがあった。それはアプリが Ruby 2.0 以降(だっけ?)にサポートしているキーワード引数を使って書いてあるのに対して、システムにインストールされているのは Ruby 1.9.3 だってこと。ユーザーは rvm でインストールした 2.3.0 を使っているのでテスト中は動くけど、いざ本番になってみるとシステムの 1.9.3 が使われるので動かない。
どうにかしてデーモンにも rvm でインストールした 2.3.0 を使わせたい。
探し方が悪いのか、ググっても情報が出てこない。結局解決策を見つけたのは、自分のエントリだった。

つまり、起動スクリプトで GEM_PATH と GEM_HOME に rvm のパスを設定して export すればいいようだ。書いた起動スクリプトがこれ:

#!/bin/sh

PATH=/home/potov/.rvm/rubies/ruby-2.3.0/bin:/home/potov/.rvm/gems/ruby-2.3.0/bin:/sbin:/bin:/usr/bin
GEM_PATH=/home/potov/.rvm/gems/ruby-2.3.0
GEM_HOME=/home/potov/.rvm/gems/ruby-2.3.0
export PATH
export GEM_PATH
export GEM_HOME

POTOV_ROOT=/home/potov/potov

case "$1" in
start)
cd ${POTOV_ROOT}
unicorn -c unicorn.conf -D
;;
stop)
PID=`cat ${POTOV_ROOT}/unicorn.pid`
kill -QUIT ${PID}
;;
*)
echo "Usage: potov {start|stop}"
exit 1
;;
esac

exit 0

当然 PATH も rvm の Ruby のパスを設定している。
ともあれ、これで無事起動するようになった。

プロセス

cf. 11 プロセス – Processes – Elixir

Elixir ではすべてのコードはプロセスの内部で動作する。このプロセスは OS のプロセスとは別物。非常に軽量で、同時に何千ものプロセスを動かすことも少なくないという。
各々のプロセスは独立、並行して動き、メッセージパッシングでやり取りする。

spawn(生み出す)

新しいプロセスを生み出すには spawn/1 を使う。

iex(1)> spawn fn -> 1 + 2 end
#PID<0.82.0>

spawn/1 は関数を引数にとり、新しいプロセスの中で実行する。関数が終わるとプロセスも死ぬ。だから上の例では新しいプロセスはすぐ死んでいるはずだ。spawn/1 は PID(プロセス識別子)を返すので、Process.alive?/1 でプロセスが生きているかどうかを確かめるてみよう。

iex(2)> pid = spawn fn -> 1 + 2 end
#PID<0.84.0>
iex(3)> Process.alive?(pid)
false

self/0 は自身の PID を返す。

iex(4)> self()
#PID<0.80.0>
iex(5)> Process.alive?(self())
true

メッセージの送信と受信

send/2 でメッセージを送り、receive/1 で受け取る。

iex(6)> send self(), {:hello, "world"}
{:hello, "world"}
iex(7)> receive do
...(7)>   {:hello, msg} -> msg
...(7)>   {:world, msg} -> "won't match"
...(7)> end
"world"

送られてきたメッセージはメールボックスの中にたまり、receive/1 はその中からマッチするメッセージを受け取る。
もしメールボックスにマッチするメッセージがなければ、マッチするメッセージがやってくるまで待ち続ける。とはいえ、待ち時間を指定することもできる。

iex(8)> receive do
...(8)>   {:hello, msg} -> msg
...(8)> after
...(8)>  1000 -> "nothing after 1s"
...(8)> end
"nothing after 1s"

上の例では、1000 ミリ秒待った後、メッセージがないという出力をしている。
つぎは、プロセス同士でメッセージを送る例。

iex(9)> parent = self()
#PID<0.80.0>
iex(10)> spawn fn -> send(parent, {:hello, self()}) end
#PID<0.100.0>
iex(11)> receive do
...(11)>   {:hello, pid} -> "Got hello from #{inspect(pid)}"
...(11)> end
"Got hello from #PID<0.100.0>"

spawn/1 で作られたプロセスから親のプロセスにメッセージを送って、親プロセスで受け取っている。

プロセスの失敗

子プロセスでエラーが起きても、親プロセスに被害は及ばない。単にエラーメッセージが表示されるだけだ。

iex(12)> spawn fn -> raise "oops" end
#PID<0.105.0>
iex(13)>
14:24:13.188 [error] Process #PID<0.105.0> raised an exception
** (RuntimeError) oops
    :erlang.apply/2

リンク

リンクを使うと事情が異なる。子プロセスで起きたエラーが親プロセスに影響を与える。spawn_link/1 を使う。

iex(13)> spawn_link fn -> raise "oops" end

14:27:12.461 [error] Process #PID<0.107.0> raised an exception
** (RuntimeError) oops
    :erlang.apply/2
** (EXIT from #PID<0.80.0>) an exception was raised:
    ** (RuntimeError) oops
        :erlang.apply/2

Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

よく見ると iex のプロンプトの番号が 1 に戻っている。これは親プロセスも一度死んで、再起動されたってことなんだろうか。

状態

Elixir のプロセスは、状態を保存しておくのにもつかわれる。

defmodule KV do

  def start_link do
    {:ok, spawn_link(fn -> loop(%{}) end)}
  end

  defp loop(map) do
    receive do
      {:get, key, caller} ->
        send caller, Map.get(map, key)
        loop(map)
      {:put, key, value} ->
        loop(Map.put(map, key, value))
      end
    end

end

上のモジュール KV を iex 上で使ってみよう。flush はメールボックスのメッセージをすべて表示してからにする関数。

^o^ > iex kv.exs
Eshell V8.0  (abort with ^G)
Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, pid} = KV.start_link
{:ok, #PID<0.86.0>}
iex(2)> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.84.0>}
iex(3)> flush
nil
:ok

KV.start_link した直後に、値をとろうとしても何もないので、flush の結果は nil になっている(:okflush 自体の返り値)。
今度は、値を put メッセージで送った後に get してみよう。

iex(4)> send pid, {:put, :hello, "world"}
{:put, :hello, "world"}
iex(5)> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.84.0>}
iex(6)> flush
"world"
:ok

こんどは “world” が返ってきている。このように、プロセスに値(状態)を保存することができる。
最後に、Elixir の提供する agent を紹介しておく。agent は上のような KV モジュールを書かなくとも、状態を簡単に保存するもの、と考えれば、とりあえずよさそうだ。

iex(7)> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.93.0>}
iex(8)> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex(9)> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world

ヴィジュネル暗号 in Elixir

Haskell でヴィジュネル暗号を解いているのを見つけた。

 cf. SECCON 2016 ヴィジュネル暗号をHaskellで解いてみる – 0x90

ヴィジュネル暗号の解説は Wikipedia に詳しい。見てみると簡単そうだったからやってみよう。もちろん Elixir で。

defmodule Vigenere do

  def encrypt(plain, key) do
    String.to_charlist(plain)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(fn {p, k} -> rem((p + k - 65 * 2), 26) + 65 end)
    |> to_string
  end

  def decrypt(cipher, key) do
    String.to_charlist(cipher)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(fn {c, k} -> rem((c - k + 26), 26) + 65 end)
    |> to_string
  end

end

IO.puts Vigenere.encrypt("CODE", "ARM")
IO.puts Vigenere.decrypt("CFPE", "ARM")
^o^ > elixir vigenere.exs
CFPE
CODE

英大文字のみに対応。65 を足したり引いたりしてるのは AZ の文字と 0 ~ 25 を対応付けるため。
さて、上記のようにうまくいったわけだけど、一つだけハマりどころがあった。それは Elixir には剰余を求める mod が見当たらないこと。代わりに余りを求める rem を使ったわけだけど、最初 mod と同じつもりで下のように書いたら、復号化に失敗した。

defmodule Vigenere do

  def encrypt(plane, key) do
    String.to_charlist(plane)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(fn {p, k} -> rem((p + k - 65 * 2), 26) + 65 end)
    |> to_string
  end

  def decrypt(cipher, key) do
    String.to_charlist(cipher)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(fn {c, k} -> rem((c - k), 26) + 65 end)
    |> to_string
  end

end

IO.puts Vigenere.encrypt("CODE", "ARM")
IO.puts Vigenere.decrypt("CFPE", "ARM")

結果はこう:

^o^ > elixir vigenere.exs
CFPE
C5DE

O になるべきところが 5 になってしまっている。原因は modrem の違い。Haskell には両方あるので試してみると:

Prelude> mod (-10) 3
2
Prelude> rem (-10) 3
-1

仕方がないので、26 を足してから rem を適用した。でもホントにこれでいいのか?

[追記]

上のスクリプトを見ると、encryptdecrypt もほとんど同じで、違うのは map の中の匿名関数だけだ。なので、共通部分をくくりだしてみた。

defmodule Vigenere do

  def encrypt(plain, key) do
    crypt(plain, key, fn {p, k} -> rem((p + k - 65 * 2), 26) + 65 end)
  end

  def decrypt(cipher, key) do
    crypt(cipher, key, fn {c, k} -> rem((c - k + 26), 26) + 65 end)
  end

  defp crypt(string, key, f) do
    String.to_charlist(string)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(f)
    |> to_string
  end

end

IO.puts Vigenere.encrypt("CODE", "ARM")
IO.puts Vigenere.decrypt("CFPE", "ARM")
^o^ > elixir vigenere2.exs
CFPE
CODE

OK。うまくいってる。

フィボナッチ数列 in Elixir

今日はフィボナッチ数列。20 項目までを出力する。

defmodule Fibonacci do

  def fib(n) do
    fib_iter(0, 1, n, 1)
  end

  defp fib_iter(a, b, n, i) do
    if i > n do
      []
    else
      [a | fib_iter(b, a + b, n, i + 1)]
    end
  end

end

IO.inspect Fibonacci.fib(20)
^o^ > elixir fib.exs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,
 4181]

上のスクリプトは次のようにも書ける。

defmodule Fibonacci do

  def fib(n), do: fib_iter(0, 1, n, 1)

  defp fib_iter(_, _, n, i) when i > n, do: []
  defp fib_iter(a, b, n, i), do: [a | fib_iter(b, a + b, n, i + 1)]

end

IO.inspect Fibonacci.fib(20)

関数の本体が十分に短ければこっちのほうが見やすいかも。
当然同じ結果になる。

^o^ > elixir fib2.exs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,
 4181]

FizzBuzz in Elixir

昨日まで約一週間、↓このサイトの GETTING STARTED をやってきた。

 cf. Elixir

1 ~ 20 までのちょうど半分、10 までやったので、ここでちょっと気分を変えて、自分でスクリプトを書いてみよう。お題は定番の FizzBuzz 問題だ。

まず最初に書いたのがこれ:

defmodule FizzBuzz do

  def fizzbuzz(n) when rem(n, 15) == 0 do
    "FizzBuzz"
  end

  def fizzbuzz(n) when rem(n, 3) == 0 do
    "Fizz"
  end

  def fizzbuzz(n) when rem(n, 5) == 0 do
    "Buzz"
  end

  def fizzbuzz(n) do
    to_string(n)
  end

  def fizzbuzz_iter(n) when n >= 100 do
    IO.puts(fizzbuzz(n))
  end

  def fizzbuzz_iter(n) do
    IO.puts(fizzbuzz(n))
    fizzbuzz_iter(n + 1)
  end

end

FizzBuzz.fizzbuzz_iter(1)

実行例は示さないけど、ちゃんと期待通りに動いた。動いたんだけど、なんというかコードがすごく冗長。関数定義が複数の句を持てるのはいいけど、これはちょっと醜いな。

で、以前 Haskell でやった剰余を使わない方法でやってみた。

defmodule FizzBuzz do

  def fb(x) do
    case x do
      {{_, "Fizz"}, "Buzz"} -> "FizzBuzz\n"
      {{_, "Fizz"}, ""} -> "Fizz\n"
      {{_, ""}, "Buzz"} -> "Buzz\n"
      {{m, ""}, ""} -> "#{to_string(m)}\n"
    end
  end

  def fizzbuzz() do
    fizz = Stream.cycle(["", "", "Fizz"])
    buzz = Stream.cycle(["", "", "", "", "Buzz"])
    1..100 |> Enum.zip(fizz) |> Enum.zip(buzz) |> Enum.map(&fb/1)
  end

end

FizzBuzz.fizzbuzz() |> IO.puts

だいぶすっきりしたけど、ネストしたタプルがあんまりよくないな。あ、出力用の文字列にいちいち改行文字がついてるのは、こうしないと全部つながって出力されるから。リストをパイプで IO.puts に渡すと、全要素を出力してから改行するらしい。そんなわけで1つ余計に改行が出力されることになったんだけど、それはまあ、いいことにする。

もう少し改良してみよう。case でパターンマッチさせるなら、何もパイプを使わずに n35 で割った余り(とn)でパターンマッチすればいい。

defmodule FizzBuzz do

  def fizzbuzz(n) do
    case {rem(n, 3), rem(n, 5), n} do
      {0, 0, _} -> "FizzBuzz\n"
      {0, _, _} -> "Fizz\n"
      {_, 0, _} -> "Buzz\n"
      {_, _, m} -> "#{to_string(m)}\n"
    end
  end

end

1..100 |> Enum.map(&FizzBuzz.fizzbuzz/1) |> IO.puts

良くなった。パターンマッチもきれいだし、いいんじゃないかな。

[追記]

Enum.each/2 を覚えた!

defmodule FizzBuzz do

  def fizzbuzz(n) do
    case {rem(n, 3), rem(n, 5), n} do
      {0, 0, _} -> "FizzBuzz"
      {0, _, _} -> "Fizz"
      {_, 0, _} -> "Buzz"
      {_, _, m} -> to_string(m)
    end
  end

end

1..100
|> Enum.map(&FizzBuzz.fizzbuzz/1)
|> Enum.each(&IO.puts/1)

これで余計な改行もなくなってきれいになった。

enumerableとstream

 cf. 10 enumerableとstream – Enumerables and Streams – Elixir

enumerable

Elixir には enumerable (列挙できる)という概念がある。例えばリストとマップは enumerable だ。Enum モジュールには、enumerable なデータを変形、並び替え、グルーピング、フィルタリング、検索するための多くの関数がある。

iex(1)> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex(2)> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]

Enum モジュールの関数は enumerable なデータなら何でも受け付ける。上の例ではリストとマップを同じ関数が受け付けている。これを、ポリモーフィックという。Enum モジュールの関数は Enumerable プロトコルを実装したどんなデータでも受け付ける……「プロトコル」については後で出て来るらしいのでここではわきに置いておく。
Elixir には範囲もある。次の例では 1..3 が範囲だ。

iex(3)> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
iex(4)> Enum.reduce(1..3, 0, &+/2)
6

貪欲vs怠惰

Enum モジュールの関数はすべて貪欲(eager)だ。enumerable なデータを受け取ってリストを返す。
……例を示す前にちょっとした準備。

iex(5)> odd? = &(rem(&1, 2) != 0)
#Function<6.52032458/1 in :erl_eval.expr/5>
iex(6)> Enum.filter(1..3, odd?)
[1, 3]

さて、貪欲とは Enum モジュールの関数を連続して使ったとき、その操作ごとに中間リストを作ることを意味している。関数を連続して使うには |> (パイプ演算子)を使う。

iex(7)> 1..100000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000

答えは出るけど、パイプごとに100000個(filter のあとは50000個)の要素を持つリストを生成している。
かわりに、Elixir には遅延操作できる Stream モジュールがある。Stream モジュールの関数は、中間リストを作る代わりに、Enum モジュールの関数に値を渡すときにだけ実行する一連の処理を作る。

iex(8)> 1..100000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000,
 funs: [#Function<46.64528250/1 in Stream.map/2>,
  #Function<39.64528250/1 in Stream.filter/2>]]>
iex(9)> 1..100000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

上の例のように、Enum.sum に渡す前の状態はリストではなく処理であって、Enum.sum に値を渡すときになって初めて処理をする。これを怠惰(lazy)であるという。このへんは Haskell の遅延評価みたいなもんだな。

Stream

Stream モジュールの関数は、引数に enumerable なデータを受け付け、結果として stream を返す。
Stream モジュールには、結果が無限になるかもしれない関数もある。たとえば Stream.cycle/1 のような:

iex(10)> Stream.cycle([1, 2, 3]) |> Enum.take(10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

再帰

cf. 9 再帰 – Recursion – Elixir

Elixir では繰り返しを再帰で行う。例えば、文字列を n 回出力するスクリプトはこんな風になる。

defmodule Recursion do

  def print_multiple_times(msg, n) when n <= 1 do
    IO.puts msg
  end
  def print_multiple_times(msg, n) do
    IO.puts msg
    print_multiple_times(msg, n - 1)
  end

end

Recursion.print_multiple_times("Hello!", 3)
^o^ > elixir recursion.exs
Hello!
Hello!
Hello!

リストの場合には [head|tail] と、終端条件には [] (空リスト)を使えばいい。

defmodule Math do

  def sum_list([], accum) do
    accum
  end

  def sum_list([head|tail], accum) do
    sum_list(tail, head + accum)
  end

end

IO.puts Math.sum_list([1, 2, 3], 0)
^o^ > elixir sum_list.exs
6

再帰についてはこれくらいでいいだろう。

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

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 モジュールを使うとキーワードリストとマップを同じように扱える。