1. ホーム
  2. スクリプト・コラム
  3. ルア

Luaにおけるイテレータとジェネリックforの使い方を徹底解説

2022-01-09 22:18:16

ジェネリックフォーの原則

イテレータとは、コレクション内のすべての要素に対して反復処理を行う仕組みのことです。Luaでは、イテレータは通常、関数として表現され、関数が呼ばれるたびにコレクション内の「次の」要素を返します。各イテレータは、呼び出しが成功するまでの間、自分がどこにいて、どのように次の位置に移動するかを知るために何らかの状態を維持する必要があり、クロージャはこれを可能にする。次の例は、リストに対する単純なイテレータである。

function values(t)
 local i = 0
 return function() i = i + 1; return t[i] end
end


ループの呼び出し

t = {10, 20, 30}
iter = values(t)
while true do
 local el = iter()
 if el == nil then break end
 print(el)
end

一般的な呼び方

for el in values(t) do print(el) end

generic forは反復ループのためのすべての帳簿を作成する。イテレータ関数を内部で保持し、反復ごとにイテレータを呼び出し、イテレータがnilを返したらループを終了させる。実際、forはイテレータ関数f、定数状態s、制御変数aの3つの値を保持します。forが最初に行うことは、inの後の式を評価して、forが保持すべき3つの値を返すことです; 次にfをsとaで呼びます。

まず、コードの一部から見てみましょう。

for element in list_iter(t) do 
 print(element) 
end 

これ以上調べない前に、すでに知っていることの構造に基づいて、このコードを理解しようとすることができます。もしそうなら、list_iter(t) はコレクションのようなものを返すはずですが、実際には無名関数であるイテレータだけを返していることが既に分かっています。もちろん、イテレータを呼び出すたびに1つの要素が得られ、イテレータの結果はすべてコレクションと見なすことができます。すべての要素が揃ったところで、論理的な説明が必要であり、その論理がgeneric forのセマンティクスである。
まず文法を見てみよう、それにはこうある。

for <var-list> in <exp-list> do 
 <body> 
end 

全体の流れはこのようになります。
まず、in以降の式の値を初期化して計算し、generic forに必要な3つの値:反復関数、状態定数、制御変数を返すようにする。多値代入と同様に、式が返す結果が3つ以下なら、自己起動するようにする。

nilで埋め、余った分は無視されます。
次に、状態定数と制御変数を引数としてイテレータ関数を呼び出します(注:for構造体の場合、状態定数は無意味なので、初期化時にその値を取得してイテレータ関数に渡せばOKです)。

3つ目は、イテレータ関数が返す値を変数リストに代入することです。
四つ目は、最初に返された値がnilの場合はループが終了し、そうでない場合はループ本体が実行される。
5つ目は、ステップ2に戻って、もう一度イテレータ関数を呼び出すことです。

具体的には

for var_1, ... , var_n in explist do block end 

と同じです。

do 
 local _f, _s, _var = explist 
 while true do 
  local var_1, ... , var_n = _f(_s, _var) 
  _var = var_1 
  if _var == nil then break end 
  block 
 end 
end 

 generic forは、反復関数、状態定数、制御変数の3つの値を自身の中に保持する。

イテレータの状態

ステートレスイテレータは、それ自身は状態を保持せず、forループは一定の状態と制御変数を持つイテレータ関数を呼び出すだけである。このタイプのイテレータの典型的な例は ipairs です。以下は、Luaによるipairsの実装です。

local function iter(s, i)
 i = i + 1
 local v = s[i]
 if v then return i, v end
end
function ipairs(s)
 return iter, s, 0
end

forループでipairs(list)を呼び出すと、3つの値を取得し、Luaはiter(list, 0)を呼び出してlist, list[1]、iter(list, 1)でlist, list[2]、をnilになるまで取得します。

generic forは状態保持のために1つの定数状態と1つの制御変数しか提供しないが、時には他の多くの状態を保持することが必要である。これはクロージャを使うか、必要な状態をテーブルに詰め込んで定常状態に保存することで実現できる。

クロージャ、イテレータ、ジェネリックフォー

さて、Luaにはクロージャ、ジェネリック・フォー、イテレータという3つの構成要素があります。ループの場合、クロージャとイテレータを使うこともできるし、ジェネリックforとイテレータを使うこともできる。では、どのようなトレードオフが必要なのでしょうか。

のアドバイスがあります。

function iter (a, i) 
 i = i + 1 
 local v = a[i] 
 if v then 
  return i, v 
 end 
end 
 
function ipairs (a) 
 return iter, a, 0 
end 
 
for i, v in ipairs(a) do 
 print(i, v) 
end 

Luaではこのケースが最も推奨されます。イテレータはupvalueに依存せず、クロージャも生成されず、状態定数と制御変数はイテレータの引数で渡されるgeneric forの助けによって保存されます。
この本からもう一つ例を挙げると

local iterator -- to be defined later 
 
function allwords() 
 local state = {line = io.read(), pos = 1} 
 return iterator, state 
end 
 
function iterator (state) 
 while state.line do -- repeat while there are lines 
  -- search for next word 
  local s, e = string.find(state.line, "%w+", state.pos) 
  if s then -- found a word? 
   -- update next position (after this word) 
   state.pos = e + 1 
   return string.sub(state.line, s, e) 
  else -- word not found 
   state.line = io.read() -- try next line... 
   state.pos = 1 -- ... from first position 
  end 
 end 
 return nil -- no more lines: end loop 
end 


これは良いアイデアなのでしょうか?Luaが出す答えは「ノー」です。この本の中に、すべてを物語る一節があります。
ループ時に状態を保存してオブジェクトを生成しない方がコストがかからないため、可能な限りステートレスイテレータを書くべきです。ステートレスイテレータを実装できない場合は、可能な限りクロージャを使用すべきです。

ステートレス・イテレータが使えない場合は、可能な限りクロージャを使うべきです。クロージャを作るコストはテーブルを作るよりも小さく、Luaはテーブルよりもクロージャを高速に処理できるため、可能な限りテーブルを使うべきではありません。