Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

Elixirのマクロを使って、FizzBuzzを書く

今日の料理

f:id:nisemono_san:20160722065057j:plain

餅とチーズが入ったお好み焼きを食べると、結構お腹が膨れることが判明した。

はじめに

何処かの誰かさんがやたらと「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のようなメッセージパッシングみたいな文法を使っていること以外、とくに目新しいものはない。ちなみに<>に関しては、文字列の結合のオペレーターになっている。

ここで、面倒だなと思うことがある。見ればわかるように、fizzbuzzの関数は、見た目は基本的に一緒なのである。このような見た目が一緒な関数をわざわざ機械的に宣言することは、人間がやるべきことではない。せいぜいタイプの数が多少減る「だけ」だとしても、そのようにコードを整理することには、多分意味がある。

マクロを利用する

というわけで、実際にマクロを利用してみよう。しかし、少し書いてみたのだけれど、マクロの宣言自体も当然のことながら先に行なっておく必要がある。そこで、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ではないのと同じである)。しかし、同時になかなか魅力的な言語であることは間違いないので、なにかあれば、また触ろうと思う。

On Lisp

On Lisp