シギル

cf. 19 シギル(印) – Sigils – Elixir

シギルとは ~ +一文字から始まる文字表現によってデータを記述するものだ。Ruby の % 記法みたいなもの。

正規表現

最もよく使うのは正規表現だ。正規表現のシギルは ~r で始まる。

iex(1)> re = ~r/foo|bar/
~r/foo|bar/
iex(2)> "foo" =~ re
true
iex(3)> "baz" =~ re
false

~r の後にセパレータ(ここでは / )が続いて、セパレータに挟まれた形で正規表現(の文字列)が来る。

セパレータ

上の例ではセパレータに / を使ったけど、Elixir では全部で8つのセパレータをサポートしている。

  • //
  • ||
  • “”
  • ()
  • []
  • {}
  • <>

内容に応じて使いやすいセパレータを使えばいい。

文字列、文字リスト、単語リスト

~s シギルは文字列を生成する。

iex(4)> ~s(hello)
"hello"

~c は文字リスト。

iex(5)> ~c(hello)
'hello'

そして ~w は単語のリストを生成する。これは Ruby の %w と同じだ。

iex(6)> ~w(foo bar baz)
["foo", "bar", "baz"]

カスタムシギル

Elixir の目標の一つに拡張性がある。シギルも拡張できる。どういうことかというと、標準では提供されていないシギルを自分で定義できるってこと。
初めに例を挙げた ~r は実際には sigil_r 関数の呼び出しになっている。

iex(7)> sigil_r(<<"foo">>, '')
~r/foo/

だから sigil_ナントカ という関数を定義すればシギルを定義したことになる。例えば整数を返す ~i を定義してみよう。

iex(8)> defmodule MySigils do
...(8)>   def sigil_i(string, []), do: String.to_integer(string)
...(8)> end
{:module, MySigils,
 <<70, 79, 82, 49, 0, 0, 5, 4, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 172,
   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, ...>>, {:sigil_i, 2}}
iex(9)> import MySigils
MySigils
iex(10)> ~i<42>
42

なるほど、期待した通りに動いている。

ところで sigil って、Perl のシジルと同じスペルなんだけど、どっちの発音が正しい(というか原語に近い)んだろうか。

内包表記

Elixir には Haskell や Python のようにリスト内包表記がある。

 cf. 18 内包表記 – Comprehensions – Elixir

iex(1)> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]

生成器とフィルタ

上の式で n <- [1, 2, 3, 4] が生成器だ。列挙可能なものなら何でも生成器の右の式に入れられる。

iex(2)> for n <- 1..4, do: n * n
[1, 4, 9, 16]

生成器はパターンマッチングに対応していて、パターンにマッチしない値は無視する。次の例では、キーワードリストからキーが :good の値だけを生成している。

iex(3)> values = [good: 1, good: 2, bad: 3, good: 4]
[good: 1, good: 2, bad: 3, good: 4]
iex(4)> for {:good, n} <- values, do: n * n
[1, 4, 16]

生成器に続けてフィルタを書くと、フィルタを通った値だけが残る。次の例は奇数だけがフィルタを通り、2乗を生成する。

iex(5)> require Integer
Integer
iex(6)> for n <- 1..4, Integer.is_odd(n), do: n * n
[1, 9]

Bitstring生成器

ビットストリングを生成することもできる。

iex(7)> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
<<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex(8)> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]

into

通常、内包表記はリストを返す。けれど :into オプションを使うことで、結果を別の型に挿入することができる。次の例は、Bitstring 生成器と :into オプションを使って、文字列から空白文字を取り除いている。

iex(9)> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"

try, catch, rescue

 cf. 17 トライ,キャッチとレスキュー – try, catch and rescue – Elixir

エラー

エラーの例は、例えばアトムに数を足そうとするとみることができる。

iex(1)> :atom + 1
** (ArithmeticError) bad argument in arithmetic expression
    :erlang.+(:atom, 1)

意図的にランタイムエラーを起こそうというときには、raise/1 マクロを使う。

iex(1)> raise "oops"
** (RuntimeError) oops

その他のエラーは raise/2 マクロにエラーの名前とキーワード引数を渡す。

iex(1)> raise ArgumentError, message: "Invalid argument foo"
** (ArgumentError) Invalid argument foo

モジュールの中で defexception/1 マクロを使うと、自分でエラーを定義することもできる。

iex(1)> defmodule MyError do
...(1)>   defexception message: "default message"
...(1)> end
{:module, MyError,
 <<70, 79, 82, 49, 0, 0, 14, 252, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 40,
   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, ...>>, :ok}
iex(2)> raise MyError
** (MyError) default message

iex(2)> raise MyError, message: "custom message"
** (MyError) custom message

try / rescue

エラー(例外)は、try / rescue 構造で拾うことができる。

iex(2)> try do
...(2)>   raise "oops"
...(2)> rescue
...(2)>   e in RuntimeError -> e
...(2)> end
%RuntimeError{message: "oops"}

上の例ではランタイムエラーを拾って、iex で表示するようにエラー自身を返している。

とはいえ、実際には try / rescue 構造を使うことはめったにないそうだ。というのも、例えば File.read/1 関数はファイルを開くとき、失敗したらエラーを起こすのではなく、「成功した」/「失敗した」という情報を含んだタプルを返すからだ。

iex(3)> File.read "hello"
{:error, :enoent}
iex(4)> File.write "hello", "world"
:ok
iex(5)> File.read "hello"
{:ok, "world"}

ファイルを開くときに起こりうる複数の結果(成功/失敗)に対応するには、単に case でパターンマッチングすればいい。

throw / catch

Elixir では後で値を捕れるように trhow / catch 構造が用意されている。たとえば、Enum モジュールで 13 の倍数となるような最初の値を見つけたいとしよう。

iex(6)> try do
...(6)>   Enum.each -50..50, fn(x) ->
...(6)>     if rem(x, 13) == 0, do: throw(x)
...(6)>   end
...(6)>   "Got nothing"
...(6)> catch
...(6)>   x -> "Got #{x}"
...(6)> end
"Got -39"

exit

Elixir ではすべてのコードは複数のプロセスの中で動いていてたがいにメッセージをやり取りしている。プロセスが死んだとき、exit という信号を送る。あるいは exit 信号を送ることで明示的に死ぬこともできる。

iex(7)> spawn_link fn -> exit(1) end
** (EXIT from #PID<0.80.0>) 1

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

exittry / catch 構造でキャッチできる。

iex(1)> try do
...(1)>   exit "I am exiting"
...(1)> catch
...(1)>   :exit, _ -> "not really"
...(1)> end
"not really"

try / catch と使うことも珍しいけど、exit をキャッチするのはもっと珍しいと書いてある。
というのも、プロセスは通常、単にプロセスの exit 信号を待つだけの監視プロセスを備えた監視ツリーにくみこまれて動作していて、監視プロセスが exit 信号を受けとると、監視対象のプロセスは再起動させられるからだ。この監視システムが、エラーの後に「早く失敗する」ことでアプリケーションを既知の初期状態へ戻すことを保証している。らしい。

after

特定の動作の後、リソースをきれいに片付けられることを保証しなければならない場合、try / after が利用できる例えばファイルを開くとき、必ず閉じることを try / after で保証できる。

iex(2)> {:ok, file} = File.open "sample", [:utf8, :write]
{:ok, #PID<0.120.0>}
iex(3)> try do
...(3)>   IO.write file, "ola"
...(3)>   raise "oops, something went wrong"
...(3)> after
...(3)>   File.close(file)
...(3)> end
** (RuntimeError) oops, something went wrong

プロトコル

 cf. 16 プロトコル – Protocols – Elixir

プロトコル

プロトコルは Elixir で多態を生み出すための仕組みだ。プロトコルの処理は、どんなデータ型であれそのデータ型がプロトコルを実装していれば処理できる。つまりこれが多態ってわけだ。
プロトコルは次のように定義する。

iex(1)> defprotocol Blank do
...(1)>   @doc "Returns true if data is considered blank/empty"
...(1)>   def blank?(data)
...(1)> end
{:module, Blank,
 <<70, 79, 82, 49, 0, 0, 18, 36, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 181,
   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, ...>>, {:__protocol__, 1}}

そしてこのプロトコルをデータ型に実装する。

iex(2)> defimpl Blank, for: Integer do
...(2)>   def blank?(_), do: false
...(2)> end
{:module, Blank.Integer,
 <<70, 79, 82, 49, 0, 0, 6, 80, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 207,
   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, ...>>, {:__impl__, 1}}
iex(3)> defimpl Blank, for: List do
...(3)>   def blank?([]), do: true
...(3)>   def blank?(_), do: false
...(3)> end
{:module, Blank.List,
 <<70, 79, 82, 49, 0, 0, 6, 100, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 211,
   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, ...>>, {:__impl__, 1}}
iex(4)> defimpl Blank, for: Map do
...(4)>   def blank?(map), do: map_size(map) == 0
...(4)> end
{:module, Blank.Map,
 <<70, 79, 82, 49, 0, 0, 6, 140, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 207,
   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, ...>>, {:__impl__, 1}}
iex(5)> defimpl Blank, for: Atom do
...(5)>   def blank?(false), do: true
...(5)>   def blank?(nil), do: true
...(5)>   def blank?(_), do: false
...(5)> end
{:module, Blank.Atom,
 <<70, 79, 82, 49, 0, 0, 6, 124, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 211,
   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, ...>>, {:__impl__, 1}}

いま、IntegerListMapAtom について Blank プロトコルを実装した。必要であればほかの型にも実装する。
それじゃ、確かめてみよう。

iex(6)> Blank.blank?(0)
false
iex(7)> Blank.blank?([])
true
iex(8)> Blank.blank?([1, 2, 3])
false

文字列に対しては実装していないのでエラーになる。

iex(9)> Blank.blank?("hello")
** (Protocol.UndefinedError) protocol Blank not implemented for "hello"
    iex:1: Blank.impl_for!/1
    iex:3: Blank.blank?/1

プロトコルと構造体

構造体はマップの拡張だけど、プロトコルの実装は共有していない。確かめるために、User 構造体を定義してみる。

iex(9)> defmodule User do
...(9)>   defstruct name: "john", age: 27
...(9)> end
{:module, User,
 <<70, 79, 82, 49, 0, 0, 8, 240, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 186,
   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, ...>>, %User{age: 27, name: "john"}}

そして確かめる。

iex(10)> Blank.blank?(%{})
true
iex(11)> Blank.blank?(%User{})
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}
    iex:1: Blank.impl_for!/1
    iex:3: Blank.blank?/1

(空の)マップに対しては true が返ってきているのに対して、User ではエラーになっている。エラーを避けるためには、User 構造体に対して Blank プロトコルを実装する必要がある(とはいえ、どういうときに true にするかわからないけど。この例ではすべて false かな)。

デフォルト実装

すべての型にプロトコルを実装するのは面倒なので、デフォルトの実装を与えておく方法がある。プロトコル定義の中で@fallback_to_anytrue に設定すると可能になる。

iex(11)> Blank.blank?(%User{})
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}
    iex:1: Blank.impl_for!/1
    iex:3: Blank.blank?/1
iex(11)> defprotocol Blank do
...(11)>   @fallback_to_any true
...(11)>   def blank?(data)
...(11)> end
warning: redefining module Blank (current version defined in memory)
  iex:11

{:module, Blank,
 <<70, 79, 82, 49, 0, 0, 18, 40, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 136,
   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, ...>>, {:__protocol__, 1}}

あー、なんか Blank モジュールを再定義したんで warning が出てるな。大丈夫かな?とりあえず進めてみる。
とにかく、上のようにプロトコルを定義して、Any に対して実装すればいい。

iex(12)> defimpl Blank, for: Any do
...(12)>   def blank?(_), do: false
...(12)> end
{:module, Blank.Any,
 <<70, 79, 82, 49, 0, 0, 6, 68, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 207,
   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, ...>>, {:__impl__, 1}}

これで、プロトコルを明示的に実装していないすべての型(構造体を含む)は、Blank.blank? に対して false を返すようになった。

iex(13)> Blank.blank?(%User{})
false

組み込みプロトコル

Elixir には組み込みのプロトコルがある。例えば Enumerable。これを実装しているデータ型には Enum モジュールの関数が使える。
また、String.Charsto_string/1 が様々な型に適用できるのはこのプロとるを実装しているからだ。
ほかにもあるらしいけど、とりあえずこのへんで。

構造体

cf. 15 構造体 – Structs – Elixir

構造体はマップの拡張だ。初期値、コンパイル時の保証、多態をもたらす、って書いてある。多態ってのはどういう意味だ?それから初期値があるのもなんだかなぁという気がする。
まあ、いい。例を見てみよう。構造体を定義するには、モジュールの中で defstruct/1 を使う。

defmodule User do

  defstruct name: "john", age: 27

end
^o^ > iex struct.exs
Eshell V8.0  (abort with ^G)
Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> %User{}
%User{age: 27, name: "john"}
iex(2)> %User{name: "meg"}
%User{age: 27, name: "meg"}
iex(3)> is_map(%User{})
true

構造体は、用意しているフィールドが存在することをコンパイル時に保証する。逆に言うと、用意していないフィールドを使おうとするとエラーになる。

iex(4)> %User{oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "john"}
    (stdlib) :maps.update(:oops, :field, %User{age: 27, name: "john"})
             struct.exs:3: anonymous fn/2 in User.__struct__/1
    (elixir) lib/enum.ex:1623: Enum."-reduce/3-lists^foldl/2-0-"/3
             expanding struct: User.__struct__/1
             iex:4: (file)

構造体はマップの拡張なので、マップと同様のアクセスができる。

iex(4)> john = %User{}
%User{age: 27, name: "john"}
iex(5)> john.name
"john"
iex(6)> meg = %{john | name: "meg"}
%User{age: 27, name: "meg"}
iex(7)> %{meg | oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "meg"}
    (stdlib) :maps.update(:oops, :field, %User{age: 27, name: "meg"})
    (stdlib) erl_eval.erl:255: anonymous fn/2 in :erl_eval.expr/5
    (stdlib) lists.erl:1263: :lists.foldl/3

パターンマッチングでもよく用いられる。

iex(7)> %User{name: name} = john
%User{age: 27, name: "john"}
iex(8)> name
"john"
iex(9)> %User{} = %{}
** (MatchError) no match of right hand side value: %{}

構造体はディクショナリではないので Dict モジュールからは使えない。

iex(9)> Dict.get(%User{}, :name)
** (UndefinedFunctionError) function User.get/3 is undefined or private
    User.get(%User{age: 27, name: "john"}, :name, nil)

モジュールのアトリビュート

cf. 14 モジュールのアトリビュート – Module attributes – Elixir

Elixir では、モジュールのアトリビュートは3つの目的に使われる。

  1. モジュールは注釈をつける
  2. 定数として利用
  3. コンパイルの際にモジュールの一時的な保管場所として利用

注釈

最もよく使われるのは2つ。これだけは覚えておこう。

  • @moduledoc – モジュールのドキュメント
  • @doc – このしるしの次にある関数やマクロのドキュメント

例えば、前に作った Math モジュールにドキュメントを追加してみよう。

defmodule Math do

  @moduledoc """
Provides math-related functions.

## Example

iex> Math.sum(1, 2)
3

"""

  @doc """
Calculates the sum of two numbers.
"""
  def sum(a, b) do
    a + b
  end

end

これをコンパイルしてから iex を立ち上げると、モジュールが読み込まれ(カレントディレクトリにあるコンパイル済みファイルは自動的に読み込まれるのを思い出すこと)、ドキュメントを見ることができる。

^o^ > elixirc math.ex

^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)> h Math
* Math

Provides math-related functions.

## Example

  iex> Math.sum(1, 2)
  3


iex(2)> h Math.sum
* def sum(a, b)

Calculates the sum of two numbers.

定数

Elixir ではしばしばアトリビュートを定数のように使う。

defmodule MyServer do

  @initial_state %{host: "127.0.0.1", port: 3456}

  IO.inspect @initial_state

end
^o^ > elixir myserver.exs
%{host: "127.0.0.1", port: 3456}

アトリビュートは関数の中でも読める。

defmodule MyServer do

  @my_data 14

  def first_data, do: @my_data

  @my_data 13

  def second_data, do: @my_data

end

IO.puts MyServer.first_data
IO.puts MyServer.second_data
^o^ > elixir myserver2.exs
14
13

一時的な貯蔵

なんか難しそうなので、パス。

alias, require, import

cf. 13 alias,requireとimpot – alias, require and import – Elixir

alias

alias はモジュールに別名をつける。例えば、数学に特化した特殊なリスト操作のモジュールを考えてみる。

defmodule Math do

  alias Math.List, as: List

end

この例では Math.List モジュールに List という別名をつけている。List へのアクセスはすべて Math.List へと展開される。オリジナルの List へは Elixir.List としてアクセスできる。

List.flatten              # => Math.List.flatten
ELixir.List.flatten       # => List.flatten
Elixir.Math.List.flatten  # => Math.List.flatten

alias はレキシカルスコープだ。あるモジュールの中で定義した alias はそのモジュールの中だけで有効。また、特定の関数の中だけで定義することも可能だ。

defmodule Math do

  def plus(a, b) do
    alias Math.List, as: List
    # ...
  end

  def minus(a, b) do
    # ...
  end

end

require

たとえば、Integer.is_odd/1 マクロを使おうという場合、Integer モジュールをプログラムに読み込まなければならない。それをするのが require ディレクティブだ。

iex(1)> Integer.is_oss(3)
** (UndefinedFunctionError) function Integer.is_oss/1 is undefined or private. Did you mean one of:

      * is_odd/1

    (elixir) Integer.is_oss(3)
iex(1)> require Integer
Integer
iex(2)> Integer.is_odd(3)
true

require もレキシカルスコープ。

import

ある別のモジュールの関数やマクロを何度も使いたとき、import すれば装飾名(モジュール名)をつけずに使うことができる。たとえば List モジュールの duplicate を何度も使いたいときはこうする:

iex(3)> import List, only: [duplicate: 2]
List
iex(4)> duplicate(:ok, 3)
[:ok, :ok, :ok]

この例では List モジュールから duplicate/2 だけを import している。only は省略可能だけど、つけることが推奨されている。only の代わりに except を使うこともできる。
only には :macros:functions を渡すこともできる。:macros を渡すと、モジュールのすべてのマクロを import する。
import もレキシカルスコープで、特定の関数内で import することもできる。

iex(3)> import List, only: [duplicate: 2]
List
iex(4)> duplicate(:ok, 3)
[:ok, :ok, :ok]

入出力

 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"

プロセス

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。うまくいってる。