目的
今回は関数型言語のElixirの特徴を感じるためにRubyとElixirでFizzBuzzを書いて見比べてみます。
FizzBuzzとは
レクリエーションから派生して1から順に数字を表示していった時に3で割り切れる場合は「Fizz」(Bizz Buzzの場合は「Bizz」)、5で割り切れる場合は「Buzz」、両者で割り切れる場合(すなわち15で割り切れる場合)は「Fizz Buzz」と表示するプログラム
環境
- OS X Yosemite
- iTem2 3.0.7
- Erlang/OTP 19 [erts-8.0.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
- Elixir 1.3.2
- ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin14]
- Pry version 0.10.3 on Ruby 2.3.0
実装 Ruby
ではfizzbuzz.rbに記述していきます。
class Integer
def fizzbuzz
case 0
when self % 15
"FizzBuzz"
when self % 3
"Fizz"
when self % 5
"buzz"
else
self
end
end
end
(7..15).each do |n|
puts "#{n}.fizzbuzz: #{n.fizzbuzz}"
end
Rubyっぽく関数に数字を渡すのではなくInteger自身にメッセージを投げて処理をするという風に書いてみました。
$eiji @ /Users/eiji/Coding/ruby:: 4:56PM
===> ruby ./fizzbuzz.rb
7.fizzbuzz: 7
8.fizzbuzz: 8
9.fizzbuzz: Fizz
10.fizzbuzz: buzz
11.fizzbuzz: 11
12.fizzbuzz: Fizz
13.fizzbuzz: 13
14.fizzbuzz: 14
15.fizzbuzz: FizzBuzz
7~15のでfizzbuzzを呼んでみてみた結果は、ちゃんとルール通りに動いていそうですね。
実装 Elixir
fizzbuzz.exsという名前で実装を用意しました。
fb = fn
0, 0, _ -> "FizzBuzz"
0, _, _ -> "Fizz"
_, 0, _ -> "Buzz"
_, _, i -> i
end
fizzbuzz = fn n -> fb.(rem(n, 3), rem(n, 5), n) end
Enum.each 7..15, fn n -> IO.puts fizzbuzz.(n) end
Elixirには条件分岐を記述できるようにif,caseやcondといった文法もあるのですが今回はそれを使わずにElixirの中で基本とされているパターンマッチというのをメインで利用して実装しました。
まず上から見て行く前にパターンマッチについて少し見ていきたいと思います。
Elixirではiexというインタラクティブモードを用意しているので確かめてみましょう。 Rubyでいうirbやpryです。
まずパターンマッチとはその名の通りマッチするパターンを調べることです。
= を使ってパターンマッチを行う事ができます。
オブジェクト指向の言語では=は代入演算子として扱われますがElixrでは代入としては扱われません。
ではIexを起動して見ていきましょう。 また違いを見てみるためにpryも起動して行きます。
$eiji @ /Users/eiji/Coding/Elixir/Programming_Elixir/part_5:: 5:28PM
===> iex
Erlang/OTP 19 [erts-8.0.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> a = 1
1
iex(2)> a = 2
2
iex(3)> a = 3
3
iex(4)> 1 = a
** (MatchError) no match of right hand side value: 3
iex(4)> 3 = a
3
一見するとaに数字を代入しているように見えるのですが、実際はマッチの過程で左辺の変数と右辺の数字が一致するように値を束縛しているだけというのがElixirの考え方です。
その証拠にaに3が束縛されている状態で1とaの=で処理してみるとMatchErrorというエラーが出力されています。また3とaを=で処理してみるとマッチしたので3が帰ってきています。
ではRubyではどうでしょうか?
$eiji @ /Users/eiji:: 5:49PM
===> pry
[1] pry(main)> a = 1
=> 1
[2] pry(main)> a = 2
=> 2
[3] pry(main)> a = 3
=> 3
[4] pry(main)> 1 = a
SyntaxError: unexpected '=', expecting end-of-input
1 = a
^
[4] pry(main)> 3 = a
SyntaxError: unexpected '=', expecting end-of-input
3 = a
^
シンタックスエラーが出てしまいます。 Rubyの中では=演算子にそのような思想はないので文法的にそのようなことが出来るように考慮されていないのがわかります。
パターンマッチがElixirの一つの特徴であることが理解できたら実際にfizzbuzzの処理を紐解いてきましょう。
fn .... end
について見ていきたいと思います。 これは無名関数つくる処理です。例えば
sum = fn a, b -> a + b end
と書いたら sumの引数を2つとり、与えられた引数を足し算するという関数を作ることが出来ます。実際にiexで動かすと
iex(5)> sum = fn a, b -> a + b end
#Function<12.52032458/2 in :erl_eval.expr/5>
iex(6)> sum.(1,2)
3
期待通りの動きをしています。ここで注意したいのは引数をaとbに束縛しているという考え方です。なので個々でもパターンマッチが行われています。ここがfb関数が条件を記述しないでfizzbuzzを出し分けているミソになります。
先ほどのsumに束縛された無名関数を少しいじってみましょう。
iex(7)> sum = fn a, 2 -> a + 2 end
#Function<12.52032458/2 in :erl_eval.expr/5>
iex(8)> sum.(1,3)
** (FunctionClauseError) no function clause matching in :erl_eval."-inside-an-interpreted-fun-"/2
iex(8)> sum.(1,2)
3
どうでしょうか2つ目の引数を2に変えた所,2以外の数字が入ってきたらマッチングのエラーが帰ってきました。これは代入ではなくパターンマッチが行われているということがわかります。fbの例を見てみると
fb = fn
0, 0, _ -> "FizzBuzz"
0, _, _ -> "Fizz"
_, 0, _ -> "Buzz"
_, _, i -> i
end
となっているのでfbは3つの引数をとり1番目、2番目の引数が0とマッチすればFizzBuzzの文字を返すという風に読めます。
_
についてですがこれはパターンマッチで値を無視するという役割があります。
iex(9)> sum = fn a, 2, _ -> a + 2 end
#Function<18.52032458/3 in :erl_eval.expr/5>
iex(10)> sum.(1,2,100)
3
iex(11)> sum.(1,2,84)
3
iex(12)> sum.(1,1,84)
** (FunctionClauseError) no function clause matching in :erl_eval."-inside-an-interpreted-fun-"/3
と見て分かる通り3つめの引数にどんな値が入ってもエラーは出ません。
ということは同様に1番目だけが0ならFizzを2番目だけが0ならBuzzを返すというふうに読めます。
どちらも0以外の数字が引数として与えられたら3番目の数字が変数iに束縛されて、その数字を返すというふうな構造になっています。
fbの構造が理解できたらfizzbuzz関数を見ていきます。
fizzbuzz = fn n -> fb.(rem(n, 3), rem(n, 5), n) end
remという関数は余りを計算する関数です。なのでfizzbuzzにセットされた無名関数は引数を一つとりそれをfbに渡します。 fbの第1引数はnを3で割った余りの数、第2引数は5で割った余り、最後はn自身の数字を渡すことでFizzBuzzのプログラムを完成させています。
例えば9であれば1番目だけ0となるのでFizzが、10であれば2番目だけ0となりBuzz、15なら1番目と2番目が0となるのでFizzBuzzといった具合に動きます。
最後にiexにを終了してfizzbuzz.exsを走らせてみると、ちゃんと動いているのが確認できます。
$eiji @ /Users/eiji/Coding/Elixir/Programming_Elixir/part_5:: 6:18PM
===> elixir fizzbuzz.exs
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
結論
RubyとElixirで実装を両方見てみましたが言語のパラダイムが違えば言語仕様も自ずと変わってきて、言語仕様が変われば同じタスクを実装するのでもコードは全く別の様相を見せるのということが実感できました。