Elixirのマクロを使って、FizzBuzzを書く
今日の料理
餅とチーズが入ったお好み焼きを食べると、結構お腹が膨れることが判明した。
はじめに
何処かの誰かさんがやたらと「Elixirはいいぞ」と煽っているので(リンクに悪意はない)、来年頃にはElixirが来るのではないかと言われて久しい(今年としないのは、もうちょっと時間が必要だと思うため)。とはいえ、ただスルーするのも勿体なくはあり、しかし同時に何かしらの触る機会というものが存在しなかった。
で、最近になって『On Lisp』という、Common Lispの解説書みたいな本を読んでいて、マクロおじさんが「マクロはいいぞ」と言っていたので、「そういえば、Elixirもマクロが使えるんだよな」と思ったので、『On Lisp』が使えるかどうかの予行練習として、マクロを使ってFizzBuzzをしてみることにした。
なぜマクロを使うのか
しかし、ただここで一つの疑問が生まれるかもしれない。そもそも、どうしてマクロを使うのだろうか。実際、マクロ自体に関しては賛否両論で、それを意図的に避けている言語もある。とりあえず、そのあたりについては放っておくとして、なぜマクロを使うのかというのがある。
まず、プログラミング言語の構文として、関数やメソッド定義では実現できないような機能がある。それは、関数自体の定義だったり、あるいはメソッド自体の定義である。あるいは、if
など、それ自体の評価が特殊な事例においての時である。もちろん、Rubyではdefine_method
みたいに、メソッドを動的に定義するためのメソッドが用意されているのだが。
上記の説明では不十分ではあるが、その言語の動作に対して手を加えたい場合、マクロは有効である。
ごくごく普通のFizzBuzz
とはいえ、いきなり「じゃあマクロを利用したFizzBuzzを作るぞ」となったとしても、そもそも自分はElixir自体を触るのははじめてなので、まず雛形を用意してやる必要がある。普通に書くとするならば、たぶん下のようになるだろう:
defmodule OldFizzBuzz do def to_s(x) do cond do rem(x, 15) == 0 -> "FizzBuzz" rem(x, 3) == 0 -> "Fizz" rem(x, 5) == 0 -> "Buzz" true -> to_string(x) end end def forprint(x) do for i <- 1..x do IO.puts to_s(i) end end end
まず、特徴的なのは、cond
という条件分岐の式が用意されていることである。これは、Lispにおけるcond
と若干様子が似ている。で、いわゆる余りを求める関数に関してはrem
を使う。これはよく見るFizzBuzzの形である。
しかし、これは冗長である。なぜなら、15は3と5の倍数なので、実際は「3の倍数であるかどうか」と「5の倍数であるかどうか」で合成ができるからである。従って、次のように書きなおすことができる。
変更版
defmodule DefultFizzBuzz do def to_s(x) do result = fizz(x) <> buzz(x) if result == "", do: to_string(x), else: result end def fizz(x) do if rem(x, 3) == 0, do: "Fizz", else: "" end def buzz(x) do if rem(x, 5) == 0, do: "Buzz", else: "" end def forprint(x) do for i <- 1..x do IO.puts to_s(i) end end end
というわけで書きなおしてみた。気になるところとしては、if
を利用する場合、SmallTalkのようなメッセージパッシングみたいな文法を使っていること以外、とくに目新しいものはない。ちなみに<>
に関しては、文字列の結合のオペレーターになっている。
ここで、面倒だなと思うことがある。見ればわかるように、fizz
とbuzz
の関数は、見た目は基本的に一緒なのである。このような見た目が一緒な関数をわざわざ機械的に宣言することは、人間がやるべきことではない。せいぜいタイプの数が多少減る「だけ」だとしても、そのようにコードを整理することには、多分意味がある。
マクロを利用する
というわけで、実際にマクロを利用してみよう。しかし、少し書いてみたのだけれど、マクロの宣言自体も当然のことながら先に行なっておく必要がある。そこで、Elixirではmodule
で、その宣言をまとめる必要がある(いいかえるならば、トップで宣言することはできない)ので、FizzBuzzHelper
というモジュールを用意し、定義する。
また、別途どのような関数を宣言したいのかをタプルによって宣言しておく。
defmodule FizzBuzzDefine do def define_taple do [{"Fizz", 3}, {"Buzz", 5}] end end defmodule FizzBuzzHelper do import FizzBuzzDefine defmacro mod_zero_string do for {name, x} <- define_taple do quote do def unquote(:"#{name}")(x) do if rem(x, unquote(x)) == 0 do unquote(name) else "" end end end end end end
まず、タプル({}
)は、同じ要素数を持つタプルの変数に対し、その値を束縛することができる。また、文字列をシンボルにしたい場合については:"hoge"
という形にする。関数をapply
で呼びだしたい場合であったり、あるいはこの場合の関数定義に利用したい場合なんかに使うとよい。
さて、気になることが一つある。それはquote
である。Lispを触っている人間ならば、「ああ、評価を遅らせるやつね」とわかるやつだ。これは、いわゆるマクロの中で実行される式と、マクロとして展開するための式を区別するためである。
とはいえ、quote
ブロックの中でも、マクロ定義の中で束縛した変数の値を使いたい場合があると思う。これについてはunquote
で、一回quote
を取り外し、評価することができるようになる。
さて、せっかくなので、ifの中身もどうせなので、マクロにしておこう。もし余りが0であるならば、指定された文字列を、そうでなければ空の文字列を返すマクロだ。
defmodule FizzBuzzIf do defmacro if_rem_0(i, x, s) do quote do if rem(unquote(i), unquote(x)) == 0, do: unquote(s) , else: "" end end end defmodule FizzBuzzHelper do import FizzBuzzDefine import FizzBuzzIf defmacro mod_zero_string do for {name, x} <- define_taple do quote do def unquote(:"#{name}")(x) do if_rem_0(x, unquote(x), unquote(name)) end end end end end
ほぼunquote
になってしまったので、恐らくマクロを利用しなくても関数で良いだろうと思う。
さて、関数の定義は終わったわけだが、まだ判定の部分、つまり3の倍数でも5の倍数でもなければ数字を返すというロジックの部分に関しては完成していない。しかし、この部分に関しては、このマクロで定義された数だけ、判定する必要があるだろう。ここではapply
の関数を使う。
defmodule FizzBuzzHelper do # ... defmacro define_to_s do functions = Enum.map(define_taple, &(elem(&1, 0))) quote do def to_s(i) do function_strings = Enum.map_join(unquote(functions), &(apply(FizzBuzz, :"#{&1}", [i]))) if function_strings == "", do: to_string(i), else: function_strings end end end end
しかし、この部分に関しては、殆どマクロを利用する必要がない。なので、これは関数に移動させる。
defmodule FizzBuzz do import FizzBuzzHelper mod_zero_string def to_s(i) do functions = Enum.map(FizzBuzzDefine.define_taple, &(elem(&1, 0))) function_strings = Enum.map_join(functions, &(apply(FizzBuzz, :"#{&1}", [i]))) if function_strings == "", do: to_string(i), else: function_strings end def forprint(x) do for i <- 1..x do IO.puts to_s(i) end end end
これで終了です。おつかれさまでした。
雑感
まず書いてみて思ったのは、案外マクロを利用する機会というか、必然性は少ないということだ。だからといって、あると便利であることは変わりはない。特に、関数では実装できないような、いわば関数自体を定義することであったり、あるいは、マクロを抽象化して、さらなるマクロを定義するのに便利であることには違いない。その一方で、なんでもマクロにしてしまうのは、明らかに暴力であるので、関数として実現できないことが出てきてマクロを使うのがベターであるというように感じる。ただ、マクロの練習として、まず関数をマクロとして書き直すのは面白いと思う。
ElixirはよくRubyと比較されがちであるけれども、Rubyが以前にJavaと比較されていたような感じで、個人的には似て似つかぬもののように感じた(Nim langがPythonのようでPythonではないのと同じである)。しかし、同時になかなか魅力的な言語であることは間違いないので、なにかあれば、また触ろうと思う。

- 作者: ポールグレアム,野田開,Paul Graham
- 出版社/メーカー: オーム社
- 発売日: 2007/03
- メディア: 単行本
- 購入: 10人 クリック: 146回
- この商品を含むブログ (128件) を見る