時代遅れひとりFizzBuzz祭り Lua編(前)

時代遅れひとりFizzBuzz祭り、今回はLua。前回のObjective Camlとの繋がりは、最近趣味で触りはじめた言語だという点。少なくとも業務都合で勉強することになった訳ではない。

Lua自体は2008年末ぐらいから注目していた。一応組込み関連の仕事をしているので、ゲーム開発で採用されているという話を聞いて「数年後に組込み機器でも採用例が出るかも」と考えていた訳だ。そしたら2009年10月にヤマハルーターRTX1200のファームウェアLuaスクリプト機能が組み込まれたとのアナウンスがあり、予想より早く採用例が出たことに驚いたのだった。

Luaはコンパクトなので、FizzBuzzで基本機能を一通り触ってみようと思う。もっとも一回のエントリでは厳しいので、前編と後編に分けることにする。

LuaについてはOCamlよりも経験値が低いので、まずオーソドックスなFizzBuzzを書いてみた。ちなみにLua 5.1.4だ。

function isnumber (n)
  if type(n) == "number" then
    return true
  elseif type(n) == "string" then
    if tostring(n) == nil then
      return false
    else
      return true
    end
  else
    return false
  end
end

function fizzbuzz (n)
  assert(isnumber(n) and n > 0, n .. " is not number or less than 1")

  if n%15 == 0 then
    return "FizzBuzz"
  elseif n%3 == 0 then
    return "Fizz"
  elseif n%5 == 0 then
    return "Buzz"
  else
    return n
  end
end

for i = 1, 100, 1 do
  print(fizzbuzz(i))
end

念の為、数値として扱える値かチェックするisnumberを定義して、assertを仕掛けている。動的型付けの意味が薄れている気もするが、気にしないことにする。

Lua演算子や制御構造はシンプルで、C言語と比べてもswitchのような多岐分岐やループ制御のcontinueが無かったりする。それでも回避方法があるので、実用上は問題ない。多岐分岐はif - elseif - elseのチェーンで書けるし、continueはVBScriptのようにループの中に1回しか実行されないダミーのループを用意することでシミュレートできる。

ちょっと驚いたのが、論理演算子のandとorでC言語の条件演算子三項演算子)を真似る書き方。

function isnumber (n)
  if type(n) == "number" then
    return true
  elseif type(n) == "string" then
    if tostring(n) == nil then
      return false
    else
      return true
    end
  else
    return false
  end
end

function fizzbuzz (n)
  assert(isnumber(n) and n > 0, n .. " is not number or less than 1")

  local val = n%3 == 0 and "Fizz" or ""
  if n%5 == 0 then val = val .. "Buzz" end

  return val == "" and n or val
end

for i = 1, 100 do
  print(fizzbuzz(i))
end

若干注意が必要な書き方だが、『Programming in Lua プログラミング言語Lua公式解説書』に載っているぐらいなので、Luaでは多分一般的な書き方なのだろう。

Luaにはデータ構造としてテーブルが用意されている。というかテーブルが唯一のデータ構造らしい。テーブルは配列や連想配列として使えるし、オブジェクトにもなる。何というかJavaScriptのオブジェクトと似ているが、当然ながらプロトタイプチェーンは無い。

function isnumber (n)
  if type(n) == "number" then
    return true
  elseif type(n) == "string" then
    if tostring(n) == nil then
      return false
    else
      return true
    end
  else
    return false
  end
end

function fizzbuzz (n)
  assert(isnumber(n) and n > 0, n .. " is not number or less than 1")

  local ref_tbl = {n, "Fizz", "Buzz", "FizzBuzz"}
  local fizz = n%3 == 0 and 1 or 0
  local buzz = n%5 == 0 and 2 or 0

  return ref_tbl[fizz + buzz + 1]
end

for i = 1, 100 do
  print(fizzbuzz(i))
end

テーブルを配列として使ってみた。Luaの流儀では添字は1から始まる。0から始まるようにも書けるが、1から始まるという前提で実装されている機能もあるので、自重しておいた方が無難だろう。

テーブルから話は逸れるが、Luaの関数はファーストクラスのオブジェクトだ。使い勝手は、これまたJavaScriptの関数に近いものがある。

function isnumber (n)
  if type(n) == "number" then
    return true
  elseif type(n) == "string" then
    if tostring(n) == nil then
      return false
    else
      return true
    end
  else
    return false
  end
end

function fizzbuzz (n)
  assert(isnumber(n) and n > 0, n .. " is not number or less than 1")

  local ref_tbl = {n, "Fizz", "Buzz", "FizzBuzz"}
  local function fizz ()
    if n%3 == 0 then return 1 else return 0 end
  end
  local function buzz ()
    if n%5 == 0 then return 2 else return 0 end
  end

  return ref_tbl[fizz() + buzz() + 1]
end

for i = 1, 100 do
  print(fizzbuzz(i))
end

関数fizzbuzzの中でローカルなクロージャを作ってみた。これも例えば、

local function fizz ()
  if n%3 == 0 then return 1 else return 0 end
end

の部分は次のようにも書ける。

local fizz = function ()
  if n%3 == 0 then return 1 else return 0 end
end

ますますJavaScriptと似ている気がしてきた。

テーブルを触ったところで、次はジェネリックforを弄くってみることにした。まずは普通にジェネリックforを使ってみた。

function isnumber (n)
  if type(n) == "number" then
    return true
  elseif type(n) == "string" then
    if tostring(n) == nil then
      return false
    else
      return true
    end
  else
    return false
  end
end

function fizzbuzz (n)
  assert(isnumber(n) and n > 0, n .. " is not number or less than 1")

  local ref_tbl = {n, "Fizz", "Buzz", "FizzBuzz"}
  local fizz = n%3 == 0 and 1 or 0
  local buzz = n%5 == 0 and 2 or 0

  return ref_tbl[fizz + buzz + 1]
end

function make_table (n, fn)
  assert(isnumber(n) and type(fn) == "function")

  local tbl = {}
  for i = 1, n do
    tbl[i] = fn(i)
  end

  return tbl
end

for i, v in ipairs(make_table(100, fizzbuzz)) do
  print(v)
end

イテレータipairsを使いたいが為に、関数make_tableを定義している。高階関数も簡単に書ける所は、やはりJavaScriptに似ている。

また話は逸れるが、Luaでは関数の末尾呼び出しが最適化される。なので末尾再帰も最適化されるはずだ。

function isnumber (n)
  if type(n) == "number" then
    return true
  elseif type(n) == "string" then
    if tostring(n) == nil then
      return false
    else
      return true
    end
  else
    return false
  end
end

function fizzbuzz (n)
  assert(isnumber(n) and n > 0, n .. " is not number or less than 1")

  local ref_tbl = {n, "Fizz", "Buzz", "FizzBuzz"}
  local fizz = n%3 == 0 and 1 or 0
  local buzz = n%5 == 0 and 2 or 0

  return ref_tbl[fizz + buzz + 1]
end

function make_table (n, fn)
  assert(isnumber(n) and type(fn) == "function")

  local function make_table_iter (n, tbl)
    if n <= 0 then
      return tbl
    else
      tbl[n] = fn(n)
      return make_table_iter(n-1, tbl)
    end
  end

  return make_table_iter(n, {})
end

for i, v in ipairs(make_table(100, fizzbuzz)) do
  print(v)
end

make_tableを末尾再帰のスタイルに書き直してみた。最適化されてスタックオーバーフローにはならないはずだ。この辺りはJavaScriptとは違う*1

今回はここまで。普通にジェネリックforを使ってみたので、次回はジェネリックforで使用するイテレータを自作してみたいと思う。

*1:正確に書くと、ECMAScript 3.0の仕様には「末尾再帰の最適化」は含まれていない。で、末尾再帰を最適化するJavaScriptの処理系はごく僅かだ。私自身、Rhinoインタープリタモードしか知らない。