普段何か簡単な処理をする時にはRubyを常用してきた。
ただそればかりじゃなく、他にも何か別の言語を使えるようになりたいなぁと思い、
数年前にLISPやScalaをインストールしてみたんだけど、
なかなか常用するぐらいには各言語の特徴を理解できず途中で放り投げていた。

そんな中、最近関数型言語に関する記事を多々見かけるようになり、
もう一度何か別の言語に手を出してみようかなと徘徊していた所、下記の記事に出会った。

[CodeIQ]「Ruby風味のプログラミング言語」四天王現る!

記事の中で関数型言語としてElixirという言語が紹介されており、
Ruby風味でちょっと面白そうなので早速インストールしてみる事にした。

試しに何か書いてみる

お題として「500から10000まで1.03倍し続けた数字を表示する」というプログラムを考えてみる。

まずはRubyから

upto.rb
# 500から10000まで1.03倍し続けた数字を表示する
# while利用バージョン
require 'pp'
num = 500
upper = 10000
rate = 1.03
list = []
while (num < upper) # upperに達するまで繰り返す
list.push num
num *= rate # ここで現在の数字を1.03倍する
end
pp list

なんかもっと良い書き方があるような気がするが、とりあえずwhileで回した。

次にElixir。
関数型言語の場合、繰り返しは再帰を用いて表現するのが好まれるようで、
見よう見まねで以下のプログラムを書いた。

upto.exs
# 500から10000まで1.03倍し続けた数字を表示する
# 再帰を使ったバージョン
defmodule Example do
def upto(begin, upper, rate) do
if begin * rate < upper do
[ begin | upto(begin * rate, upper, rate) ]
else
[ begin ]
end
end
end
base = 500
upper = 10000
rate = 1.03
Example.upto(base, upper, rate)
|> Enum.map(&(IO.puts(&1)))

求める結果は得られたが、再帰を使い慣れていない為か、書くのに時間がかかってしまった。

さらに言うなら結構行数が多くてどうもしっくり来ない。
もっとお手軽に表現できないもんかとElixir関連の記事を漁っていた所、
下記の無限リストが使えそうだと思い参考にしてみた。

[Qiita]無限リストによるエラトステネスのふるい

上の記事を参考にして書き直したのが以下。

upto2.exs
# 500から10000まで1.03倍し続けた数字を表示する
# 無限リストを利用したバージョン その1
base = 500
upper = 10000
rate = 1.03
Stream.iterate(base, &(&1 * rate)) # base(500)からスタートして1.03倍し続けた無限リストを作り...
|> Stream.filter(&(&1 < upper)) # 上記無限リストからupper(10000)未満の条件でフィルタする
|> Enum.map(&(IO.puts(&1))) # 後はフィルタされたリストを表示するだけ

短くはなったが、ここで問題が発生。実行すると以下のエラーが出てしまった。

upto2.exs 実行結果
$ elixir upto2.exs
500
515.0
530.45
546.3635
...
(中略)
...
9057.701942151143
9329.433000415678
9609.31599042815
9897.595470140994
** (ArithmeticError) bad argument in arithmetic expression
upto2.exs:8: anonymous fn/1 in :elixir_compiler_0.__FILE__/1
(elixir) lib/stream.ex:1016: anonymous fn/2 in Stream.iterate/2
(elixir) lib/stream.ex:1189: Stream.do_unfold/4
(elixir) lib/stream.ex:1240: Enumerable.Stream.do_each/4
(elixir) lib/enum.ex:1391: Enum.reduce/3
(elixir) lib/enum.ex:1047: Enum.map/2

どうも10000までのリストは得られているようだが、8行目の処理でエラーが出ているという表示だ。
Stream.filterではなくEnum.takeで個数を指定するとエラーが出なくなる。
filterを使って条件でリストを取得しようとするとエラーが出る。filter/2ではダメなんだろうか。

参考にした無限リストのサンプルは、Enum.take(10)などでリストから何個取得するか明示している。
が、今回のお題だと条件にマッチするリストが元から何個あるのかわからないのでtake/2は使えない。

他にもStreamを使っているサンプルを探してみたが、
take/2で何個取得するのが明示してあるものが多くエラー解消に結びつかない。

英語だからと避けていた公式ドキュメントで、改めて使えそうなものがないか物色。
[Elixir公式]take_while(collection, fun) … 名前がそれっぽいので説明を見る。

Takes the items from the beginning of collection while fun returns a truthy value.

これじゃね?

ということで9行目Stream.filterの部分を書き換える。

upto3.exs
# 500から10000まで1.03倍し続けた数字を表示する
# 無限リストを利用したバージョン その2
base = 500
upper = 10000
rate = 1.03
Stream.iterate(base, &(&1 * rate)) # base(500)からスタートして1.03倍し続けた無限リストを作り...
|> Enum.take_while(&(&1 < upper)) # upperを超えるまで数字を取得し続ける
|> Enum.map(&(IO.puts(&1)))
upto3.exs 実行結果
$ elixir upto3.exs
500
515.0
530.45
546.3635
...
(中略)
...
9057.701942151143
9329.433000415678
9609.31599042815
9897.595470140994
(エラーなし)

教訓: 公式ドキュメントを見るのは大切ですね

今回Ruby以外の言語も使えるようになりたいという事でElixirを触ってみましたが、
パイプライン演算子Stream(遅延評価)など面白い要素があり勉強になりました。
これからも、もう少し色々試してみようと思います。