Elixirを使ってみる

久々にブログの順番がまわってきました、渕上です。
入社して初めて触ったPHPフレームワークは’Ethna(えすな)’でした。
正直なところよく覚えていないのですが、名前が素敵だなと感じたのを覚えています。

今回ブログのテーマを使ったことのない言語にしてみようと調べてみたところ、
‘Elixir(えりくさー)’という言語のことを知りました。

「えすな」に「えりくさー」。なんだか運命的な出会いだと思いませんか。
今回のテーマはElixirに決めました。

※註:「エスナ」は恐らく某超大作RPGの魔法の名前から命名されており、「エリクサー」という言葉も同じゲームにアイテム名として登場します
※註:私はどちらかというと「Sa・Ga」と「メガテン」派でした

Elixirとは?

WikipediaによるとElixirは次のような特徴をもつ言語だそうです。

Elixir (エリクサー) は並列処理の機能や関数型といった特徴を持つ、Erlangの仮想環境 (BEAM) 上で動作するコンピュータプログラミング言語である。

https://ja.wikipedia.org/wiki/Elixir_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E)

インストールする

Erlangのインストール

rpmからインストールしようと思ったのですが、依存関係の解決に必要なRPMForgeが停止しているらしいので、ソースからインストールを行いました
現時点(2016年9月)の最新版をインストールします

$ wget http://erlang.org/download/otp_src_19.0.tar.gz
$ tar zxvf otp_src_19.0.tar.gz
$ make
$ sudo make install

Elixirのインストール

githubから入手、インストールしました。

$ wget https://github.com/elixir-lang/elixir/releases/download/v1.3.2/Precompiled.zip -O Precompiled.zip 
$ sudo unzip Precompiled.zip -d /opt/elixir
$ export PATH=/opt/elixir/bin:$PATH

Hello World

Elixirは次の3パターンで実行できるそうです。

  1. 対話形式の実行
  2. スクリプトファイルから実行
  3. コンパイルしたファイルから実行

紙面の都合上、3を割愛して1と2をご紹介します。

Elixirを対話形式で実行する

iexというコマンドで対話形式で実行ができます
とりあえず、”Hello World”を出力してみたいと思います

$ iex
Erlang/OTP 19 [erts-8.0] [source] [64-bit] [async-threads:10] [hipe]
[kernel-poll:false]

Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> IO.puts('Hello World')
Hello World
:ok

スクリプトから実行する

Elixirをスクリプトファイルから実行する場合は、
スクリプトファイルを保存して「elixir」コマンドに渡します。
今回も、”Hello World”を出力してみます。

  • hello.exs
IO.puts('Hello World')
  • 実行
$ elixir hello.exs
Hello World

FizzBuzz

Elixirで書かれたFizzBuzzの例をいくつか眺めていたのですが、初見で全く理解できなかったものがありました。
(Functional FizzBuzz in Elixir)
そのソースコードを下記に転載させていただきます。

defmodule FizzBuzz do
  def print(n) do
    generate
    |> Stream.take(n)
    |> Stream.each(&IO.puts/1)
    |> Stream.run
  end

  def generate do
    fizzes = Stream.cycle(["Fizz", "", ""])
    buzzes = Stream.cycle(["Buzz", "", "", "", ""])

    Stream.zip(fizzes, buzzes)
    |> Stream.map(&concat/1)
    |> Stream.with_index
    |> Stream.map(&translate/1)
    |> Stream.drop(1)
  end

  defp concat({f, b}), do: f <> b

  defp translate({"",   n}), do: to_string(n)
  defp translate({str, _n}), do: str
end

えーっと、全く意味が解りません。

関数型言語やElixirを知っている人ならば一目なのでしょうか。
関数型とか手続き型とか関係なく、頭の作りの問題なのでしょうか。
とにかく私にとっては尖がったワンライナーくらい、よくわかりません。

Elixirの記事を書こうと決めた時に少しだけ情報を目にしていたので、

  • defmodule -> モジュールの定義
  • def -> モジュールの関数の定義
  • defp -> モジュールのプライベート関数の定義

というのは知っていたのですが、その他のことがさっぱりです。
辛うじて私にわかるのは、

  1. FizzBuzzモジュールが定義されていること
  2. print, generate, concat, translate 関数が定義されていること
  3. FizzBuzz.print(50)のような方法で実行できそうなこと
  4. |>とかStreamって何?

ということくらいです。

当初は何かサンプルコードを書いてExlirの制御文やデータ型について調べていこうと思っていたのですが、
予定変更して、このFizzBuzzコードを読み解いていくことにします。

このFizzBuzzで何が行われているのか

パイプ演算子(|>)

ソースコードを眺めてみると、まず目につくのが見慣れない記号(|>)です
Elixirでは処理を(|>)でつなげることによって処理結果を次の関数の第一引数に渡せるそうです。
シェルのパイプと同様ですね。 なんだか急に親しみがわいてきました。

つまり、FizzBuzz.printでは

  1. FizzBuzz.generateを実行
  2. 結果をStream.takeに渡して実行
  3. 結果をStream.eachに渡して実行
  4. 結果をStream.runに渡して実行

しているわけですね。 う~ん、よくわかりません。
恐らく次に知るべきはStreamがなんなのかということです。

Stream

Streamはリストなどに関する処理を行う組み込みモジュールです。
似たモジュールにEnumというものがあるのですが、Enumは即時実行されるのに対してStreamは遅延(Lazy)実行になります。

たとえば[1,2,3]という数値のリストから最初の要素を返すときに、
Enumでは1という数値を返しますが、Streamでは「あとで最初の要素返すから!」という約束を返すというイメージですかね。

※ Elixir超初心者の私にはうまく言葉にする術がありませんので、公式ドキュメントやその翻訳をご覧ください。
* http://elixir-lang.org/docs/stable/elixir/Stream.html#cycle/1
* http://elixir-ja.sena-net.works/getting_started/10.html

リストの要素をそれぞれ二乗するという処理を行って、違いを比べてみたいと思います。
Enum.map, Stream.map でリストの各要素に対してコールバックを適用することができます。

EnumとStreamでPHPでいうところの下記コードを書いていきたいと思います。

array_map(function($n){ return $n*$n; }, array(1,2,3,4,5));

まずはEnum.mapで実行してみました。

iex(1)> [1,2,3,4,5] |> Enum.map(fn n -> n*n end)
[1, 4, 9, 16, 25]

戻り値は、各要素を二乗したリストになっています。

次にStream.mapです

iex(12)> [1,2,3,4,5] |> Stream.map(fn n -> n*n end)
#Stream<[enum: [1, 2, 3, 4, 5],
 funs: [#Function<46.89908360/1 in Stream.map/2>]]>

さっきと戻り値が違いますね。Stream型のデータ構造が返って来ているんでしょうか。
この結果(Stream)をEnum.to_listすると、Streamをリストとして評価することになり、リスト自体が返ってきます。
試してみましょう。

iex(13)> [1,2,3,4,5] |> Stream.map(fn n -> n*n end) |> Enum.to_list
[1, 4, 9, 16, 25]

リストになりました!

同名の関数の定義

再び、ソースコードに目を向けてみます。
今度は、以下の部分が気になりました。

  defp translate({"",   n}), do: to_string(n)
  defp translate({str, _n}), do: str

defpは、プライベート関数の定義を行います。translateという名前の関数が2回定義されていますね。

PHPやJavascriptに慣れ親しんだ私からすると、奇妙な感じに思えます。
PHPならば実行時エラーが発生しますし、Javascriptならば定義が上書きされてしまいますよね。

しかしElixirではそうではないそうです。
モジュール内に同名の関数がある場合、定義した引数と実際の引数をマッチして、最初にマッチしたものを実行するみたい。
早速、簡単なスクリプトを作って実行してみましょう。

  • test.exs
defmodule Test do
  def check (1) do
    IO.puts('これは「イチ」です');
  end

  def check (2) do
    IO.puts('これは「に」です');
  end

  def check (num) do
    IO.puts(num);
  end
end

Test.check 1
Test.check 2
Test.check 3
Test.check 4

上の「test.exs」の実行結果は次のようになります。

$ elixir test.exs
これは「イチ」です
これは「に」です
3
4

もう一度、FizzBuzz.translateを見てみます。

  defp translate({"",   n}), do: to_string(n)
  defp translate({str, _n}), do: str

(中かっこで表されるのは、「タプル」というデータ型です。)
(参考:http://elixir-ja.sena-net.works/getting_started/2.html#2.7-%E3%82%BF%E3%83%97%E3%83%AB—tuples)

この関数は、

  1. タプルの0番目の要素が空文字の場合は、1番目の要素を文字列で返す
  2. タプルの0番目の要素がその他の場合は、0番目の要素を返す

ということをするように定義されているんですね。
恐らくなのですが、↓のようなタプルがわたってくるのだと思います。
数字を、Fizz or Buzz or FizzBuzz の文字列に変換する部分なんじゃないでしょうか。

{'', 1}          -> 1
{'', 2}          -> 2
{'Fizz', 3}      -> Fizz

再度、FizzBuzzを読み解く

FizzBuzz.generateがやっていること

  def generate do
    fizzes = Stream.cycle(["Fizz", "", ""])            # 1
    buzzes = Stream.cycle(["Buzz", "", "", "", ""])    # 1

    Stream.zip(fizzes, buzzes)                         # 2
    |> Stream.map(&concat/1)                           # 3
    |> Stream.with_index                               # 4
    |> Stream.map(&translate/1)                        # 5
    |> Stream.drop(1)                                  # 6
  end

少しずつ謎が解けてきたので、FizzBuzz.generateが何をしているのか読み解いていきます。

1: Stream.cycleは与えられた引数を無限に繰り返します。
3回に1回”Fizz”が出現する繰り返しと、5回に1回Buzzが出現する繰り返しを定義しています。

2: Stream.zipで2つのStream(fizzes, buzzes)を合わせた、タプルの繰り返しにします。
言葉にしてもよくわからないので、リストに変換した結果を見てみましょう。

iex(2)> fizzes = Stream.cycle(["Fizz", "", ""])
#Function<59.89908360/2 in Stream.unfold/2>
iex(3)> buzzes = Stream.cycle(["Buzz", "", "", "", ""])
#Function<59.89908360/2 in Stream.unfold/2>
iex(4)> Stream.zip(fizzes, buzzes) |> Enum.take(10);
[{"Fizz", "Buzz"}, {"", ""}, {"", ""}, {"Fizz", ""}, {"", ""}, {"", "Buzz"},
 {"Fizz", ""}, {"", ""}, {"", ""}, {"Fizz", ""}]
  • 15回に1回 {“Fizz”, “Buzz”}
  • 3回に1回 {“Fizz”, “”}
  • 5回に1回 {“”, “Buzz”}
  • その他の時 {“”, “”}
    が出現するリストが生成されました。(実際にはまだリストになっていませんが、リストに変換したものを見たいので、Enum.takeで指定の個数だけリストとして取り出しました。)

3: Stream.mapでそれぞれの要素に対してコールバックを適用します。コールバック(defp concat)では、タプル0と1番目の要素を文字列結合しています。
“Fizz”or”Buzz”or”FizzBuzz”or””を定期的に繰り返すStreamが出来上がりました。

iex(1)> fizzes = Stream.cycle(["Fizz", "", ""])
#Function<59.89908360/2 in Stream.unfold/2>
iex(2)> buzzes = Stream.cycle(["Buzz", "", "", "", ""])
#Function<59.89908360/2 in Stream.unfold/2>
iex(3)> Stream.zip(fizzes, buzzes)|> Stream.map(fn {f, b} -> f<> b end) |> Enum.take(15)
["FizzBuzz", "", "", "Fizz", "", "Buzz", "Fizz", "", "", "Fizz", "Buzz", "",
 "Fizz", "", ""]

4: Stream.with_indexで、それぞれの要素を0番目の要素、インデックスを1番目の要素にしたタプルのリストに変換します。

iex(33)> Stream.zip(fizzes, buzzes)|> Stream.map(fn {f, b} -> f<> b end) |> Stream.with_index |> Enum.take(15)
[{"FizzBuzz", 0}, {"", 1}, {"", 2}, {"Fizz", 3}, {"", 4}, {"Buzz", 5},
 {"Fizz", 6}, {"", 7}, {"", 8}, {"Fizz", 9}, {"Buzz", 10}, {"", 11},
 {"Fizz", 12}, {"", 13}, {"", 14}]

5: もう一度Stream.mapでコールバックを適用します。コールバックは前述した(defp translate)です
3のリストの空文字部分が、with_indexで付けたインデックスで埋められたStreamが完成しました
"FizzBuzz", 1, 2, "Fizz", 4, "Buzz".................といった繰り返しになっているはずです。

6.: 最後にStrem.dropでStreamの先頭の1つの要素を破棄します(Streamは遅延実行なので、破棄することを約束するといった表現になるのでしょうか。)

ここまでの処理で、

  • 1から始まり、1ずつ増えていく数字の要素をもつ
  • 3で割り切れるときは”Fizz”になる
  • 5で割り切れるときは”Buzz”になる
  • 3でも5でも割り切れるときは”FizzBuzz”になる

というFizzBuzzの性質を持つStreamが完成しました。

FizzBuzz.printがやっていること

  def print(n) do
    generate
    |> Stream.take(n)
    |> Stream.each(&IO.puts/1)
    |> Stream.run
  end

FizzBuzz.printでは、FizzBuzz.generateしたStreamに対して次の処理がパイプで繋がれています

  1. generateしたStreamの要素をn個取り出して
  2. 要素を順番に出力
  3. Streamを実行

FizzBuzz.printを実行してみます。

  • fzbz.exs
defmodule FizzBuzz do
  def print(n) do
    generate
    |> Stream.take(n)
    |> Stream.each(&IO.puts/1)
    |> Stream.run
  end

  def generate do
    fizzes = Stream.cycle(["Fizz", "", ""])
    buzzes = Stream.cycle(["Buzz", "", "", "", ""])

    Stream.zip(fizzes, buzzes)
    |> Stream.map(&concat/1)
    |> Stream.with_index
    |> Stream.map(&translate/1)
    |> Stream.drop(1)
  end

  defp concat({f, b}), do: f <> b

  defp translate({"",   n}), do: to_string(n)
  defp translate({str, _n}), do: str
end

FizzBuzz.print(50);
  • 実行結果
$ elixir fzbz.exs
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
31
32
Fizz
34
Buzz
Fizz
37
38
Fizz
Buzz
41
Fizz
43
44
FizzBuzz
46
47
Fizz
49
Buzz

いかがだったでしょうか、今回はFunctional FizzBuzz in Elixirのソースコードを読み解くことによって、Elixirの触りを体験してみました。

実のところ、Elixirの良さはほとんど理解できていないと思うのですが、、
普段使っている言語と全く違うコードを読むのは楽しい時間でした。
(FizzBuzz.translateが何を意味しているか気づけたとき、うれしかったです。)

次回なのですが、Elixirで動くWebフレームワークがあるそうなので、それを触ってみようかと思います。