ようへいの日々精進XP

よかろうもん

ゴールデンウィークスペシャル:Elixir チュートリアル (1) 〜パターンマッチ編〜

以前から

気になっていた Elixir という言語について、以下の資料を写経してチュートリアルしてみた。

www.slideshare.net

その前に Elixir (エリクサー) とは

そこには Erlang が…

  • Erlang の関数を呼び出せるため、Erlang のシステムにシームレスに統合出来る
  • Erlang 良い部分(並行性や信頼、耐障害性)をそのまま利用出来る(当然 Erlang の悪い部分も受け継いでしまっているらしい)

特徴

その他色々… こちらが参考になる。

エコシステム

  • ライブラリ管理に hex を利用する… Ruby だと gem
  • ビルドツールに mix を利用する… Ruby だと rake
  • 対話型シェルに iex を利用する… Ruby だと irb
  • Web Application Framework として Phoenix がメジャー… Ruby だと Rails

で、なんであんたは Elixir をやろうとしたの?

  • Ruby と似た文法ということで、以前から気になっていた
  • ギョームでたまに Erlang というワードが耳に入ってくる

チュートリアル

チュートリアル環境

  • OS は Ubuntu Xenial でお届けします
ubuntu@ubuntu-xenial:~$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
  • Elixir は 1.4.1 でお届けします
ubuntu@ubuntu-xenial:~$ elixir --version
Erlang/OTP 19 [erts-8.3] [source-d5c06c6] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Elixir 1.4.1

Elixir のインストール

以下を実行して Erlang と Elixir をインストールする。

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb
sudo apt-get update
sudo apt-get install esl-erlang
sudo apt-get install elixir

以下を実行して iex を起動することを確認する。

ubuntu@ubuntu-xenial:~$ iex
Erlang/OTP 19 [erts-8.3] [source-d5c06c6] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help)

対話シェルで操作してみる

対話シェル iex を起動して加算や list や map の操作を行ってみる。

iex(1)> 1+2
3
iex(2)> list = [ 123, "abc", 456, true ]
[123, "abc", 456, true]
iex(3)> Enum.sort( list )
[123, 456, true, "abc"]
iex(4)> map = %{ "key2" => "abc", "key1" => 123 }
%{"key1" => 123, "key2" => "abc"}
iex(5)> Enum.sort( map )
[{"key1", 123}, {"key2", "abc"}]
iex(6)>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution

list や map の操作に Enum モジュールを利用している。Enum モジュール には eachempty? 等の関数が用意されていて、さながら Ruby と見紛う。

関数を定義する

新規プロジェクトの作成

新規プロジェクトを以下のように作成する。

mix new sample

以下のように出力される。

* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/sample.ex
* creating test
* creating test/test_helper.exs
* creating test/sample_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd sample
    mix test

Run "mix help" for more commands.

関数の作成

lib/sample.ex を以下のように記述する。

defmodule Sample do
  def sort( values ) do
    Enum.sort( values )
  end
end

関数のビルドと実行

記述したら、以下のコマンドを実行してビルドを行う。

cd sample
iex -S mix

iex が起動するので、以下のように作成した Sample 関数を実行する。

ubuntu@ubuntu-xenial:/vagrant/elixir/sample$ iex -S mix
Erlang/OTP 19 [erts-8.3] [source-d5c06c6] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Sample.sort( [ 5, 1, 3 ])
[1, 3, 5]
iex(2)> Sample.sort [ 5, 1, 3 ] #=> () は省略可能
[1, 3, 5]

おお。

関数の修正と再ビルド

関数は end を省略して 1 行でも記載することが出来る。

defmodule Sample do
  def sort( values ), do: Enum.sort( values )
end

関数を修正したら recompile() を実行して再ビルドする。

iex(3)> recompile()
Compiling 1 file (.ex)
:ok

改めて Sample 関数を実行する。

iex(4)> Sample.sort [ 5, 1, 3 ]
[1, 3, 5]
iex(5)> Sample.sort %{ "key2" => "abc", "key1" => 123 }
[{"key1", 123}, {"key2", "abc"}]

map もキー名でソートすることが出来る。

パターンマッチ

引数でのマッチ

Sample 関数に以下を追加する。

def match( %{ Yes: need } ), do: need

追加したら recompile() する。

iex(7)> recompile()
Compiling 1 file (.ex)
:ok

以下のように Sample 関数を実行する。

iex(8)> Sample.match %{ No: "-", Yes: "we can", NA: "N/A" }
"we can"

マップの Yes キーの値を関数の引数だけで実装出来る。ちなみに、Ruby でやろうとすると、以下のような書き方になるんだろうか。

irb(main):001:0> def match(args = {})
irb(main):002:1> p args[:Yes]
irb(main):003:1> end
=> :match
irb(main):004:0> match({ No: "-", Yes: "we can", NA: "N/A" })
"we can"
=> "we can"

あー、改めて、関数の引数だけで値が取り出せるのは便利かも。

複数関数の呼び分け

値でもマッチさせることも可能なので、以下を追加して recompile() する。

def match( %{ Yes: "we can" } ), do: "Barack Obama" # 追加する
def match( %{ Yes: need } ), do: need

Sample 関数を実行してみる。

iex(2)> Sample.match %{ No: "-", Yes: "we can", NA: "N/A" }
"Barack Obama"

ほー。マップのキーをチェックして処理を分岐することが出来ている。これも Ruby でやろうとすると以下のような書き方になるんだろうか。

irb(main):001:0> def match(args = {})
irb(main):002:1> if args[:Yes] == "we can"
irb(main):003:2> p "Barack Obama"
irb(main):004:2> end
irb(main):005:1> end
=> :match
irb(main):006:0> match({ No: "-", Yes: "we can", NA: "N/A" })
"Barack Obama"
=> "Barack Obama"

マッチしない場合

キーが存在しない場合には、以下のようなエラーとなる。

iex(4)> Sample.match %{ No: "-", NA: "N/A" }
** (FunctionClauseError) no function clause matching in Sample.match/1
    (sample) lib/sample.ex:24: Sample.match(%{NA: "N/A", No: "-"})

以下のように関数の引数に _ を指定すると、その他としてマッチ出来るようになる。

def match( %{ Yes: "we can" } ), do: "Barack Obama"
def match( %{ Yes: need } ), do: need
def match(_), do: "Yes...Not EXIST" # 追加する

recompile() して関数を再実行する。

iex(5)> recompile()
Compiling 1 file (.ex)
:ok
iex(6)> Sample.match %{ No: "-", NA: "N/A" }
"Yes...Not EXIST"

例の如く、Ruby で書くと以下のようになるんだろうか。

irb(main):010:0> def match(args = {})
irb(main):011:1> p "Yes...Not EXIST" unless args.has_key?(:Yes)
irb(main):012:1> end
=> :match
irb(main):013:0> match({ No: "-", NA: "N/A" })
"Yes...Not EXIST"
=> "Yes...Not EXIST"

関数内でマッチ

関数の引数では無く、関数内でマッチさせる場合には以下のように書く。

defmodule Sample do
  def match_inner( input_map ) do
    %{ Yes: need } = input_map
    need
  end
end

以下のように引数マッチと同じ結果となる。

iex(10)> Sample.match_inner %{ No: "-", Yes: "we can", NA: "N/A" }
"we can"

引数マッチ版の方がシンプルな気がする。

リストのパターンマッチ

リストは先頭を head で、以降を tail でパターンマッチすることが出来る。

iex(1)> [ head | tail ] = [ 5, 8, 3, 1 ]
[5, 8, 3, 1]
iex(2)> head
5
iex(3)> tail
[8, 3, 1]

わかり易い言葉で書くと head はリストの最初の要素を取り出すことが出来て、tail はその残りを取り出すことが出来る。

以下のようにも書くことが出来るようだ。

iex(10)> list = [1,2,3]
[1, 2, 3]
iex(11)> hd(list)
1
iex(12)> tl(list)
[2, 3]

Ruby だと以下のように書くんだろうか。

irb(main):014:0> list = [ 5, 8, 3, 1 ]
=> [5, 8, 3, 1]
irb(main):015:0> list.first
=> 5
irb(main):016:0> list[-3..-1]
=> [8, 3, 1]

個人的には Elixir も Ruby も直感的な気がする。各コード量的には微々たるものだけど Elixir の方が少ないかもしれない。

リストのパターンマッチ(2)

headtail を利用することで、リストの巡回(再帰呼び出し)が可能となる。

defmodule Sample do
  def nums( [ head | tail ], rows ) do #...1
    nums( tail, [ head | rows ] )      #...2
  end
  def nums( [], rows ) do              #...3
    rows                               #...4
  end
end

関数を実行すると以下のように出力される。

iex(6)> Sample.nums( [ 5, 8, 3, 1 ], [] )
[1, 3, 8, 5]

少し理解に苦しんだので、以下のように整理した。

  1. リストの先頭を head で、残りを tail でマッチさせる
  2. tail に入っている残りの値の先頭を head でマッチさせる
  3. 2 の tail が空になったら 3. が呼ばれで 4. の rows が出力される

正直言って、まだボヤーっとしているので、headtail はもう少し弄る。

ちなみに、Ruby だと以下のように書くのかな…

irb(main):006:0> list = [5, 8, 3, 1]
=> [5, 8, 3, 1]
irb(main):007:0> new_list = []
=> []
irb(main):008:0> list.each { |l| new_list << l }
=> [5, 8, 3, 1]
irb(main):009:0> new_list.reverse
=> [1, 3, 8, 5]

リストのパターンマッチ(2) + 1

もすこし再帰呼び出しをやりたいので、list に格納されている数値の合計を求めてみる。

defmodule Sample do
  def sum_list( [ head | tail ], total ) do #...1
    sum_list( tail, head + total )          #...2
  end
  def sum_list( [], total ) do              #...3
    total                                   #...4
  end
end

おお、なんか理解が出来てきたけど整理。

  1. リストの先頭を head で、残りを tail でマッチさせる(初期値は 0)
  2. 1 の tailheadtotal に格納されている数値を加算(tail が空になるまで繰り返す)
  3. 2 の tail が空になったら 3. が呼ばれて 4. の total が出力される

Ruby だと inject メソッドを使って以下のように書くはず。

irb(main):010:0> list = [5, 8, 3, 1]
irb(main):011:0> list.inject(0) { | sum, l | sum + l }
=> 17

おお、シンプル。

ということで

sample.ex

今回のチュートリアルで作成した sample.ex は以下のようになった。

defmodule Sample do
  # def sort( values ) do
  #   Enum.sort( values )
  # end
  def sort( values ), do: Enum.sort( values )
  def match( %{ Yes: "we can" } ), do: "Barack Obama"
  def match( %{ Yes: need } ), do: need
  def match(_), do: "Yes...Not EXIST"
  def match_inner( input_map ) do
    %{ Yes: need } = input_map
    need
  end
  # def nums( [ head | tail ], rows ), do: nums( tail, [ head | rows ] )
  # def nums( [], rows ), do: rows
  def nums( [ head | tail ], rows ) do #...1
    nums( tail, [ head | rows ] )      #...2
  end
  def nums( [], rows ) do              #...3
    rows                               #...4
  end

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

Elixir とは (再掲)

引き続き…

参考にさせて頂いた資料はまだまだ続くので、引き続き写経を続けていくぞー。

参考