イテレータパターン
CoddyのLuaジャーニー「Object Oriented Programming」セクションの一部 — レッスン 64/70。
オブジェクト内に格納された項目のコレクションがある場合、それらをループ処理したいことがよくあります。Luaの汎用forループはカスタムイテレータとうまく連携し、この機能をクラスに直接組み込むことができます。
イテレータパターンは、オブジェクトがその内部データの走査方法を制御できるようにします。エレガントなアプローチの1つは、__call メタメソッドを使用することです。これにより、オブジェクトを関数のよう呼び出し可能にします。各呼び出しは、項目がなくなるまで次の項目を返します。
local Collection = {}
Collection.__index = Collection
function Collection:new()
local obj = { items = {}, index = 0 }
setmetatable(obj, Collection)
return obj
end
function Collection:add(item)
table.insert(self.items, item)
end
function Collection:__call()
self.index = self.index + 1
return self.items[self.index]
end
__call が定義されていると、while ループを使用して反復処理を行ったり、オブジェクトを直接汎用的な for に渡したりすることができます:
local bag = Collection:new()
bag:add("apple")
bag:add("banana")
for item in bag do
print(item)
end
ループは bag() を繰り返し呼び出します。__call が nil (これ以上アイテムがない状態)を返すと、ループは停止します。このパターンは、反復ロジックをオブジェクト自体の内部にカプセル化し、アイテムがどのように、そしてどのような順序でアクセスされるかを完全に制御できるようにします。
チャレンジ
簡単Luaの汎用forループを使って反復処理ができるPlaylistクラスを作成しましょう!__callメタメソッドを使用してイテレータパターンを実装し、プレイリストを曲ごとにトラバースできるようにします。
コードは2つのファイルに分けて構成します:
Playlist.lua: 内部に曲を保存するPlaylistクラスを作成します。このクラスには、空の曲リストを初期化し、反復インデックスを0に設定する:new()コンストラクタが必要です。プレイリストに曲を追加するための:addSong(title)メソッドを追加してください。最も重要なのは、プレイリストが「呼び出される」たびにリスト内の次の曲を返す(曲がなくなったらnilを返す)ように__callメタメソッドを実装することです。main.lua: Playlistモジュールをロード(require)し、新しいプレイリストを作成します。受け取った入力に基づいて曲を追加し、汎用forループを使用してプレイリストを反復処理し、各曲のタイトルをそれぞれの行に出力します。
イテレータパターンの魔法は、Playlistオブジェクトが呼び出し可能になることです。forループで使用されると、Luaはnilが返されるまでそれを繰り返し呼び出し、各曲を順番に取得します。
プレイリストに追加する3つの曲のタイトルが入力として与えられます(順番通り)。
例えば、入力が Bohemian Rhapsody、Stairway to Heaven、Hotel California の場合、出力は以下のようになります:
Bohemian Rhapsody
Stairway to Heaven
Hotel California入力が Yesterday、Imagine、Let It Be の場合、出力は以下のようになります:
Yesterday
Imagine
Let It Beチートシート
イテレータパターン(Iterator pattern)は、カスタムの反復ロジックを実装することで、オブジェクトが内部データの走査方法を制御できるようにします。
__call メタメソッドを使用して、オブジェクトを関数のよう呼び出し可能にします。各呼び出しは、項目がなくなるまで(nil を返すまで)次の項目を返します。
local Collection = {}
Collection.__index = Collection
function Collection:new()
local obj = { items = {}, index = 0 }
setmetatable(obj, Collection)
return obj
end
function Collection:add(item)
table.insert(self.items, item)
end
function Collection:__call()
self.index = self.index + 1
return self.items[self.index]
end
__call を定義すると、汎用的な for ループの中でオブジェクトを直接使用できます。
local bag = Collection:new()
bag:add("apple")
bag:add("banana")
for item in bag do
print(item)
end
ループはオブジェクトを繰り返し呼び出します。__call が nil を返すと、ループは自動的に停止します。
自分で試してみよう
-- Playlistモジュールを要求する
local Playlist = require('Playlist')
-- 入力から3つの曲名を読み込む
local song1 = io.read()
local song2 = io.read()
local song3 = io.read()
-- TODO: Playlist:new() を使用して新しいプレイリストを作成する
-- TODO: :addSong() を使用して3つの曲をプレイリストに追加する
-- TODO: 汎用forループを使用してプレイリストを反復処理し、
-- 各曲名をそれぞれの行に表示する
-- ヒント: for song in playlist do ... end
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。