時代遅れひとりFizzBuzz祭り Objective Caml編

時代遅れひとりFizzBuzz祭り、今回はObjective Caml。前回のRubyとの繋がりは、「Rubyに次ぐツール自作用言語として期待しているプログラミング言語」という点。全く個人的な理由だが、気にしてはいけない。

どの辺りを期待しているのかというと、デフォルトでネイティブコンパイラが用意されている所。Windows上で他人に配布するツールを作れそう。RubyPerlなどのスクリプト言語の難点は「インタプリタが無いと動かない」所で、特にWindowsユーザの場合、どんなに便利なツールを書いても誰も使ってくれない可能性が高い。その点、ネイティブな実行ファイルだと安心して配布できる。

ただ1点だけ気がかりなのは日本語周りの取り扱いで、Windowsコマンドプロンプトで日本語文字列が化けずに出力できるのかどうかイマイチ分からない。Windows上のOCaml開発環境が無くなってしまったので特に分からない。EUC-JPやUTF8でコードを書いたとして、print_stringで日本語を含む文字列を出力するとコマンドプロンプトではどうなるのだろう?

OCamlは最近触り始めた言語なので、取り敢えず手続き型的な発想はそのままに、OCamlの文法で書いてみた。ちなみにUbuntu 8.04のOCaml 3.10.0を使っている。

let fizzbuzz n =
  assert (n > 0);
  let ref_tbl = [| string_of_int n; "Fizz"; "Buzz"; "FizzBuzz" |] in
  let fizz = if n mod 3 = 0 then 1 else 0 in
  let buzz = if n mod 5 = 0 then 2 else 0 in
    ref_tbl.(fizz + buzz);;

for i = 1 to 100 do
  print_string (fizzbuzz i ^ "\n")
done

案の定OCamlっぽくないコードだ。配列を参照テーブルにしている所がOCaml的でないと思う。おもいっきりループで書いている点は、一応OCaml的ではあるけど、関数型言語的ではないように感じる。

OCamlといえばパターンマッチングだ。配列を参照テーブルとする代わりにパターンマッチングの書き方にできるだろう。forループの部分も、Schemeみたいに*1関数の組み合わせで実現できるような素敵機能がどこかにあるはずだ。

ということで書き直してみた。

let fizzbuzz n =
  assert (n > 0);
  let fizz = if n mod 3 = 0 then 1 else 0 in
  let buzz = if n mod 5 = 0 then 2 else 0 in
    match (fizz + buzz) with
      | 1 -> "Fizz"
      | 2 -> "Buzz"
      | 3 -> "FizzBuzz"
      | otherwise -> string_of_int n;;

Array.iter (fun x -> print_string (fizzbuzz x ^ "\n")) (Array.init 100 (fun x -> x + 1))

パターンマッチングの書き方にした意味があるのか無いのか微妙なところ。あと正直に書くと、最後の1行はどう考えてもここからコピペしたコードとしか思えない。

悔しいので、わざともう一工程増やしてみた。

let fizzbuzz n =
  assert (n > 0);
  let fizz = if n mod 3 = 0 then 1 else 0 in
  let buzz = if n mod 5 = 0 then 2 else 0 in
    match (fizz + buzz) with
      | 1 -> "Fizz"
      | 2 -> "Buzz"
      | 3 -> "FizzBuzz"
      | otherwise -> string_of_int n;;

Array.iter (fun x -> print_string (x ^ "\n"))
  (Array.map fizzbuzz (Array.init 100 (fun x -> x + 1)))

1〜100までの値が格納された配列を生成し、その値をFizzBuzz用に変換した新しい配列を生成し、その要素を表示している。

ここにきて、2つ前のコードに違和感を覚えた。print_stringで出力する時に関数fizzbuzzで変換するのではなく、事前にfizzbuzzで変換した文字列の配列を作っておく方がよいのではないか? つまり、こんな感じ。

let fizzbuzz n =
  assert (n > 0);
  let fizz = if n mod 3 = 0 then 1 else 0 in
  let buzz = if n mod 5 = 0 then 2 else 0 in
    match (fizz + buzz) with
      | 1 -> "Fizz"
      | 2 -> "Buzz"
      | 3 -> "FizzBuzz"
      | otherwise -> string_of_int n;;

Array.iter (fun x -> print_string (x ^ "\n")) (Array.init 100 (fun x -> fizzbuzz (x + 1)))

少なくとも、この方が個人的に好みだ。

さて、ここまでは配列を使ってきたのだけど、OCamlといえばリストだろう。ということでリストを使うように変更してみた。

let fizzbuzz n =
  assert (n > 0);
  let fizz = if n mod 3 = 0 then 1 else 0 in
  let buzz = if n mod 5 = 0 then 2 else 0 in
    match (fizz + buzz) with
      | 1 -> "Fizz"
      | 2 -> "Buzz"
      | 3 -> "FizzBuzz"
      | otherwise -> string_of_int n

let make_list n fn =
  let rec make_list_iter n list =
    if n <= 0 then list
    else make_list_iter (n - 1) (fn (n - 1) :: list)
  in
    make_list_iter n [];;

List.iter (fun x -> print_string (x ^ "\n")) (make_list 100 (fun x -> fizzbuzz (x + 1)))

Array.initに相当するものを見つけられなかったので、自前で関数を用意している。Array.initで生成した配列をArray.to_listでリストにしようかとも考えたけど、それはそれで意味がないので*2やめた。関数型言語らしく再帰で書いてみたのだが、なぜか末尾再帰的なイメージしか浮かばなかった。

*1:「Lisp脳」の謎に迫る - Schemeプログラマの発想あたりを念頭においている。

*2:このプログラムでは変換するメリットが無い。というか別に配列のままで構わないので。