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

id:eel3:20091213:1260716184 の続き。予告通り、後編である。

前回、普通にジェネリック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 countup (begin, max)
  assert(isnumber(begin) and isnumber(max) and begin <= max)

  local i = begin - 1
  return function ()
    if (i < max) then i = i + 1; return i end
  end
end

for i in countup(1, 100) do
  print(fizzbuzz(i))
end

初期値beginから終了値maxまでカウントアップするイテレータcountupだ。取り敢えず教科書通り(?)にクロージャを返す実装になっている。

イテレータの中にはステートレスイテレータというものがある。ジェネリックforの仕組みを利用することでクロージャのような内部状態を持たなくても動作するイテレータで、標準ライブラリのipairsなどが該当するらしい。クロージャを生成するコストが掛からない訳で、オーバヘッドが大きく響く場合にはステートレスイテレータ化が効きそうだ。

自作したcountupは特に複雑な内部状態を持っている訳ではないので、ステートレスイテレータ化してみた。

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 countup (begin, max)
  assert(isnumber(begin) and isnumber(max) and begin <= max)

  local iter = function (max, i)
    if (i < max) then return i + 1 end
  end

  return iter, max, begin - 1
end

for i in countup(1, 100) do
  print(fizzbuzz(i))
end

この実装ではクロージャではなく単なる関数を返している。同時にカウンタ変数の初期値と最大値も返していることに注意。今までクロージャ内で保持していたカウンタ変数と最大値は、イテレータ呼び出し時にジェネリック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 countup (begin, max)
  assert(isnumber(begin) and isnumber(max) and begin <= max)

  local state = {count = begin, maximum = max}
  local iter = function (state)
    local i, max = state.count, state.maximum
    if (i <= max) then
      state.count = i + 1
      return i
    end
  end

  return iter, state
end

for i in countup(1, 100) do
  print(fizzbuzz(i))
end

理屈は簡単で、例えばcountupの場合、カウンタ変数と最大値を引数として渡すのではなく、必要な内部状態を保持するテーブルを渡すようにすればよい。但し『Programming in Lua プログラミング言語Lua公式解説書』によると、複雑な状態を持つイテレータを実装する場合、テーブルを使う方法よりクロージャの方がエレガント且つ効率的らしい。同書を読む限り、コスト面では、

  1. ステートレスイテレータ
  2. クロージャによるイテレータ
  3. テーブルを使ったイテレータ
  4. コルーチンによるイテレータ

といった感じだろうか*1。「可能ならステートレスイテレータにすべき」ということらしい。

ここまでテーブル、関数とクロージャジェネリックfor、イテレータLua組み込みの機能を一巡りしてきたので、そろそろコルーチンなんぞ体験してみようと思う。我が人生で初めてのコルーチンである*2

題材は引き続きイテレータ。コルーチンを内包する関数を返すように書き直してみた。

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 countup (begin, max)
  assert(isnumber(begin) and isnumber(max) and begin <= max)

  return coroutine.wrap(function ()
    for i = begin, max do
      coroutine.yield(i)
    end
  end)
end

for i in countup(1, 100) do
  print(fizzbuzz(i))
end

この例では、コルーチンを使ったバージョンの方がイテレータ本体の挙動が分かりやすいと思う。コルーチンを使わない実装では、イテレータ本体のコードだけではイマイチ挙動が掴みにくい。ジェネリック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 countup (begin, max)
  assert(isnumber(begin) and isnumber(max) and begin <= max)

  local iter = coroutine.wrap(function (max, begin)
    for i = begin, max do
      coroutine.yield(i)
    end
  end)

  return iter, max, begin
end

for i in countup(1, 100) do
  print(fizzbuzz(i))
end

ちょうどステートレスイテレータ版を変形した感じだが、イテレータの最初の呼び出し時の引数のみ使い、それ以降は引数に指定された値を無視する。この辺り、最初の内は混乱する人もいるかもしれない。

コルーチンも触ったことなので、最後の仕上げとしてCで書いたプログラムとの連携を試してみた。もっとも今回は安直に、

call_lua.exe fizzbuzz.lua

Luaで書いたFizzBuzzスクリプトを実行するだけ。

/**
 * @file   call_lua.c
 * @brief  引数指定した Luaスクリプトを読み込み、実行する。
 *         引数に何も指定しなかった場合は標準入力からスクリプトを読み込む。
 * @note   call_lua [File..]
 */

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"


/** ファイルパスの区切り文字 */
#define  PATH_SEP  '\\'


/** ファイル名からディレクトリ部分を取り除いた先頭位置を返す */
static const char *basename(const char *name)
{
	const char *bn;

	assert(name != NULL);

	bn = strrchr(name, PATH_SEP);
	return (bn == NULL) ? name : bn+1;
}

/** スタックに積まれたエラーメッセージを表示する。
 *  その後、スタックからエラーメッセージを取り除く。
 */
static void warn(lua_State *lua, const char *s)
{
	const char *err;

	assert(lua != NULL);

	if (lua_gettop(lua) == 0) {
		return;     /* スタックが空 */
	}

	if (!lua_isstring(lua, -1)) {
		return;     /* 文字列ではない */
	}

	err = lua_tostring(lua, -1);
	if (err == NULL) {
		return;
	}

	(void) fprintf(stderr, "%s: %s\n", s, err);
	lua_pop(lua, 1);
}

/** Lua スクリプトを読み込み、実行する。
 *  失敗したらエラーメッセージを表示し、プログラムの終了値を変更する。
 */
#define DO_LUA_SCRIPT(lua, fname)               \
        do {                                    \
        	if (luaL_dofile(lua, fname)) {      \
        		warn(lua, basename(argv[0]));   \
        		retval = EXIT_FAILURE;          \
        	}                                   \
        } while (/*CONSTCOND*/ 0)

/** メインルーチン */
int main(int argc, char *argv[])
{
	lua_State *lua;
	int i, retval;

	/* Lua の言語エンジンを初期化 */
	lua = luaL_newstate();
	if (lua == NULL) {
		(void) fprintf(stderr, "%s: no memory\n", basename(argv[0]));
		return EXIT_FAILURE;
	}

	/* Lua の標準ライブラリを使えるようにする */
	luaL_openlibs(lua);

	/* Lua スクリプトを読み込み、実行 */
	retval = EXIT_SUCCESS;
	if (argc <= 1) {
		DO_LUA_SCRIPT(lua, NULL);
	} else {
		for (i = 1; i < argc; ++i) {
			DO_LUA_SCRIPT(lua, argv[i]);
		}
	}

	/* 後片付け */
	lua_close(lua);

	return retval;
}

ところどころ、あまりまともではない書き方かもしれない。特にマクロの使い方が微妙で、人によっては拒否反応を示すと思う。

ついでにWindows上でMinGWでビルドする用のMakefileも晒しておく。これまたサフィックス周りとか色々と微妙にまともではなくて、多分MSYSとか導入済みのWindows上でしか使えない。

.SUFFIXES: .exe

CMDS		=	call_lua.exe

#CURRDIR	=	${.CURDIR}
CURRDIR		=	.

LUADIR		=	$(CURDIR)/lua

CC			=	gcc
CHARSET		=	-finput-charset=cp932 -fexec-charset=cp932
DEBUG		=	-g
INCLUDES	=	-I$(LUADIR)/include
PROFILE		=	-fprofile-arcs -ftest-coverage

WARN		=	-Wall -ansi -pedantic -pedantic-errors
WARN		+=	-Wshadow -Wpointer-arith -Wsign-compare -Wstrict-prototypes -Wmissing-declarations -Wmissing-prototypes
WARN		+=	-Wextra -Wformat=2 -Wcast-qual -Wcast-align -Wwrite-strings -Wfloat-equal
WARN		+=	-Wstrict-aliasing=2 -Wdeclaration-after-statement -Wbad-function-cast -Winline
WARN		+=	-Winit-self -Wswitch-default -Wswitch-enum -Wundef -Wno-multichar -Wunreachable-code -Wunused-macros
#WARN		+=	-Wredundant-decls -Wconversion -Wc++-compat

CFLAGS		=	$(WARN) $(CHARSET) $(INCLUDES)
RM			=	rm -f



all:	$(CMDS)

.c.exe:
	$(CC) $(CFLAGS) -o $@ $< $(LUADIR)/lib/lua51.dll

clean:
	$(RM) $(CMDS)

LUADIR、INCLUDES、.c.exeでのLuaのDLLのパスを調整すること。なおWARNに大量の警告フラグを追加しているのは個人的な趣味なので、右から左に受け流してほしい。

*1:テーブル版とコルーチン版のコスト差の順番は間違っているかもしれない。

*2:……誰か『初めてのコルーチン』なんて記事とか書いてたりして :)