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

Luaチュートリアル(V)。イテレータとジェネリック・フォー

2022-02-13 16:22:08

1. イテレータとクロージャ。

    Luaでは、イテレータは通常、関数であり、関数を呼び出すたびにコレクション内の"next"要素を返します。各イテレータは、呼び出しが成功するまでの間に何らかの状態を保持する必要があり、自分がどこにいて、次に走査されたときにどこにいるかが分かるようになっています。この観点から、Luaのクロージャ・メカニズムは、次の例に示すように、この問題に対する言語的な安全策を提供します。

コピーコード コードは以下の通りです。

function values(t)
    local i = 0
    return function()
        i = i + 1
        return t[i]
    end
end
t = {10, 20, 30}
it = values(t)
while true do
    local element = it()
    if element == nil then
        break
    end
    print(element)
end
--another foreach-based call (generic for)
t2 = {15, 25, 35}
for element in values(t2) do
    print(element)
end
-- The output is.
--10
--20
--30
--15
--25
--35

  上記の応用例から、whileアプローチよりもgeneric forアプローチの方が、より明確な実装ロジックを提供します。これは、Luaがイテレータ関数を内部に保持し、イテレータがnilを返してループが終了するまで、各反復でその暗黙の内部イテレータを呼び出すからです。

    2. 汎用forのセマンティクス

    上の例のイテレータは、ループするたびに新しいクロージャ変数を作成する必要があり、そうしないと、最初の反復が成功した後に新しいforループで使われたときに、クロージャが単に終了してしまうという明らかな欠点があるのです。

    ここでは、Luaにおけるジェネリック(for)の仕組みを詳しく説明し、その後にステートレスイテレータの例を挙げて理解を深めていきます。イテレータの実装がステートレス・イテレータであれば、ジェネリック(for)ごとに新しいイテレータ変数を再宣言する必要はありません。
    generic (for) 型の構文は以下の通りです。

コピーコード コードは以下の通りです。

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

    実用的なアプリケーションである <exp-list> では、通常は式(expr)を含むだけなので、ここでは簡単のために、式のリストではなく、式を含むだけの記述にします。ここでは、まず、以下のような式のプロトタイプと例を示します。
コピーコード コードは以下の通りです。

function ipairs2(a)
    return iter,a,0
end

    この関数は3つの値を返します。1つ目は実際のイテレータ関数変数、2つ目は定数オブジェクトで、ここではトラバースされるコンテナと解釈できます。3つ目の変数はiter()関数が呼ばれたときに渡される初期値です。
    ここで、再びiter()関数の実装を見てみると、次のようになります。
コピーコード コードは以下の通りです。

local function iter(a, i)
    i = i + 1
    local v = a[i]
    if v then
        return i, v
    else
        return nil, nil
    end
end

イテレータ関数iter()では、テーブルのキーと値に対応する2つの値を返します。キー(返されたi)がnilの場合、汎用(for)は、この反復が終了したと見なします。次のような実用的なユースケースを見てみましょう。

コピーコード コードは以下の通りです。

function ipairs2(a)
    return iter,a,0
end


local function iter(a, i)
    i = i + 1
    local v = a[i]
    if v then
        return i, v
    else
        return nil, nil
    end
end

a = {"one","two","three"}
for k,v in ipairs2(a) do
    print(k, v)
end
-- The output is.
-1 one
-2 two
-3 three

この例での汎用的な(for)書き方は、例えば次のようなwhileループを使った方法に拡張することができる。

コピーコード コードは以下の通りです。

local function iter(a, i)
    i = i + 1
    local v = a[i]
    if v then
        return i, v
    else
        return nil, nil
    end
end

function ipairs2(a)
    return iter,a,0
end

a = {"one","two","three"}
do
    local _it,_s,_var = ipairs2(a)
    while true do
        local var_1,var_2 = _it(_s,_var)
        _var = var_1
        if _var == nil then -- Note that here only the first one returned by the iterator function is determined if it is nil.
            break
        end
        print(var_1,var_2)
    end
end
-- Output is as above.


3. ステートレスイテレータの例
    この例では、チェーンテーブルをトラバースするイテレータを実装します。
コピーコード コードは以下の通りです。

local function getnext(list, node) -- Iterator function.
    if not node then
        return list
    else
        return node.next
    end
end

function traverse(list) --extension of generic(for)
    return getnext,list,nil
end

--Initialize the data in the chain table.
list = nil
for line in io.lines() do
    line = { val = line, next = list}
end

-- Iterate through the chain table as a generic (for).
for node in traverse(list) do
    print(node.val)
end

 ここで使われているトリックは、チェーンの先頭のノードを定数状態(トラバースによって返される2番目の値)、現在のノードを制御変数として使うことです。イテレータ関数 getnext() が最初に呼ばれたとき、node は nil なので、この関数は list を最初のノードとして返します。それ以降の呼び出しでは、nodeはもはやnilではないので、イテレータはチェーンの最後にnilノードを返すまでnode.nextを返し、その時点でジェネリック(for)はイテレータ・トラバーサルが終了したことを判断する。

    最後の注意点として、traverse()関数とリスト変数は、新しいクロージャ変数を作成することなく、繰り返し呼び出すことが可能です。これは主にイテレータ関数(getnext)がステートレスイテレータとして実装されているためです。

    4. 複雑な状態を持つイテレータ

    前述のイテレータの実装では、イテレータは多くの状態を保持する必要がありますが、ジェネリック(for)型は状態を保持するための定数状態と制御変数しか提供しません。最も簡単な方法はクロージャを使うことですが、もちろん、すべての情報をテーブルにカプセル化して、それを定状態オブジェクトとしてイテレータに渡すことも可能です。定常状態変数自体は一定、つまり反復中に別のオブジェクトに変化することはありませんが、そのオブジェクトに含まれるデータが変化するかどうかは、完全にイテレータの実装に依存します。とりあえず、table型の定数オブジェクトはイテレータが依存するすべての情報をすでに含んでいるので、イテレータは総称(for)が提供する第2引数を完全に無視することができる。次のコードに見られるように、そのようなイテレータの例を以下に示します。

コピーコード コードは以下の通りです。

 local iterator
function allwords()
    local state { line = io.read(), pos = 1 }
    return iterator, state
end
The --iterator function will be the real iterator
function iterator(state)
    while state.line do
        local s,e = string.find(state.line,"%w+",state.pos)
        if s then
            state.pos = e + 1
            return string.sub(state.line,s,e)
        else
            state.line = io.read()
            state.pos = 1
        end
    end
    return nil
end