はじめに
最近、
グローバル変数に頼ることなく「関数単独で状態遷移を実現できる関数」を作って遊んでいます。
具体的には次のようなの。
- 配列を与えると、それを環状配列(左端と右端がつながっているリング状配列、ドラクエのワールドマップをイメージするといいかも)と解釈し、実行するたびにその環状配列を右、または左に順に辿って参照できる関数を返す関数。
--initRingは、状態遷移関数を作成する関数 --元となる配列と初期参照位置を与える ring = initRing({'a', 'b', 'c'}, 1) -- 状態遷移関数に与える引数で参照位置を動かす -- 正の数ならば参照位置をその値だけ右にずらす、マイナスは左にずらす -- 環状配列なので'c'の次は'a'、'a'の前は'c'となる ring(0) =>a ring(1) =>b ring(1) =>c ring(1) =>a ring(-1) =>c
関数自身が現在の遷移状態を保持しているので、シンプルに使えるところが特徴ですね。
たとえば関数の外側に状態保持用の変数(たとえば、pos = 1など)を作って関数内でそれを操作すれば同じことを実現できますが、他の場所で変数posに直接アクセスされて書き換えられると困るんですよね。
データの隠蔽は近代プログラミング作法の基本ですし(大げさ)。
それに、たとえば状態遷移関数を10個作成(ring1~ring10)したら、状態保持用の変数も10個管理(pos1~10)しなくてはならず、管理が面倒ですし。
状態遷移関数の実装-クロージャ版
実装方法としては、クロージャーを使う方法とコルーチンを使う方法の2通りがあるのですが、
とりあえず両方並べて見ます。
ソースコード
クロージャを使って作ったのがこちら。
function initRing1(array, i) -- iは参照する初期位置 return function (x) i= i+x return array[((i-1)%#array)+1] end end cl = initRing1({'a','b','c'}, 1) print(cl(0)) print(cl(1)) print(cl(1)) print(cl(1)) print(cl(-1))
関数initRing1()は、無名関数を作成する関数です。
しかも作成された無名関数は、arrayとiという2つの特殊な変数を持っています。
クロージャについてもっと詳しく!
これだけじゃ何言っているかわからないですよね。つまりぶっちゃけて言いますと、
上記でinitRing1によって作成された関数「cl」は、実質的に、以下のdo~endのスコープ内の記述と同じような働きをする関数です。
do local array = {'a','b','c'} local i = 1 return function(x) i= i+x return array[((i-1)%#array)+1] end end
initRing1()を実行するたびに、上記do~endスコープの間にあるような、「特殊なローカル変数2つ」と「無名関数」をセットでまとめた関数を作成します。
ちなみにこのローカル変数の何が特殊かと言うと、initRing1()で新しい関数を作成するたびに、その関数に付属する形でローカル変数が作成されることです。
- a = initRing1({'a', 'b', 'c'}, 1)
- 関数aは、ローカル変数Aと無名関数Aを含む
- b = initRing1({'a', 'b', 'c'}, 1)
- 関数bは、ローカル変数Bと無名関数Bを含む
- c = initRing1({'a', 'b', 'c'}, 1)
- 関数cは、ローカル変数Cと無名関数Cを含む
たとえば、上記のように3個の関数「a」「b」「c」を作ったときに、それぞれに含まれるローカル変数A,B,Cは全て個別に作られ、独立しています*1。そのため、関数a,b,cはすべて独立して動かすことができるのです。
Luaでは、このような特殊なローカル変数のことを「上位値」と呼んでいます。
そしてこのような「上位値」と「無名関数」をセットにした関数を作成する「機能」のことをクロージャと呼んでいます*2。
このクロージャはとても強力な機能でして、関数型言語では「データの隠蔽」「再帰関数における状態保持」などの用途で使われます。
今回は、「データ隠蔽」のためにクロージャを使い、状態遷移関数を作成したわけです。
補足
上記コード内で暗号のように見える以下の計算式は、変数iの値によって最小値1から最大値が配列の要素数(#array)という範囲の整数を返します。
((i-1) % #array)+1
サンプルコードの例では、与える配列{'a','b','c'}の要素数が3なので、iの値が何であっても、1~3までの値を返します*3。
array[4]では値がありませんからね。array[4]をarray[1]に変換することで、循環しているように見せかけているわけです。
状態遷移関数の実装-コルーチン版
コルーチンを使って作ったのがこちら。
さすがに説明するスペースがないので、今回は解説を省略します。
コルーチンについては、そのうちしっかりと解説しましょう。
function initRing2(array,i) -- iは参照する初期位置 return coroutine.wrap(function() while true do i= i + coroutine.yield(array[((i-1)%#array)+1]) end end) end -- funcは、中断したスレッドを再開させる関数 func = initRing2({'a','b','c'}, 1) func() --関数実行スタート --以下、実行のたびに中断したスレッドを再開 --引数は参照位置をずらすための値 print(func(0)) print(func(1)) print(func(1)) print(func(1)) print(func(-1))
coroutine.wrap()は便利だけど、作られる関数funcの挙動が・・・・・・ちょっとだけきもいw
関数実行スタートするとき(初回)と、中断したスレッドを再開するとき(二回目以降)で、引数の意味が違ってくるとかね・・・・・・。
coroutine.create()やcoroutine.resume()を使った方が理解しやすいコードになるとは思うけれど、今回はこれが本題ではないので省略。
まとめ
Lua公式本によると、パフォーマンス(実行スピード)はcoroutine再開と関数呼び出しで差はほとんどないとのことなので、どちらを採用するかは好み次第でしょうね。
シンプルさではやはりクロージャーかな。
coroutineも嫌いではないけど、デバッグが面倒すぎるのでw
え?このような状態遷移関数を何に使うかですって?
以前作った、turtle.selectNext()関数を書き換えられるかなと思いまして。
【補足2】既存の関数を上書きして拡張しよう - Minecraftとタートルと僕
以前の実装だと、現在の選択スロット番号を保持する隠しパラメータ(グローバル変数)を必要としていたけれど、
このような状態遷移関数があれば隠しパラメータが必要なくなりますよね。データ隠蔽。データ隠蔽。
具体的なコードについては、次回紹介しましょう。
- (2014.01.31追記)