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。「可能ならステートレスイテレータにすべき」ということらしい。
ここまでテーブル、関数とクロージャ、ジェネリック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に大量の警告フラグを追加しているのは個人的な趣味なので、右から左に受け流してほしい。