LoginSignup
12
13

More than 5 years have passed since last update.

ジェネリックfor文を使って独自のイテレータを作る

Last updated at Posted at 2014-08-20

Lua5.1のfor文について、Lua公式解説本を参考にしつつまとめてみました。

まずは数値用for文について

1から10までiの値を1ずつ増加させるだけですので、説明は必要ないですよね。

for i=1,10 do
  print(i)
end

少しだけ面白いのは変化量を指定できる点。

以下の書き方で、100から1までiの値を1ずつ減らしながら回すことができます。

for i=100,1,-1 do
  print(i)
end

ジェネリック(汎用)for文について

よく使うのは、テーブルの中身を表示するための以下の記法。

local tbl = {a=1,b=2,c=3}
for key,value in pairs(tbl) do
  print(key,' = ',value)
end

テーブルではなく配列だと以下のようなやり方をよく見ます。

local array = {1,2,3,4,5}
for i,value in ipairs(array) do
  print(i,' = ',value)
end

この pairs()やipairs()はイテレータ関数と呼ばれるとても便利な関数です。

このイテレータ関数を自作することにより独自のイテレータをつくることができます。

イテレータ関数を作る方法はいくつかあるのですが、ここではもっともシンプルで理解しやすいということでクロージャを利用する方法をご紹介します。

なお、このイテレータ関数という呼び方ですが、誤解を招く表現なので私は余り好きではありません。

ジェネリックfor文が要求するクロージャを作成するための関数なので、Lua公式本に従い、以降はファクトリと呼びます。

具体的にファクトリは次のような仕様を満たすような関数であればなんでもかまいません。

  • ファクトリは、クロージャ(上位値と無名関数を含む)を返す関数である
  • クロージャは実行するたびに値を返すこと。値は複数個でも可。それら値はジェネリックfor文の変数として使われる
  • この変数のうち、先頭の変数は制御変数と呼ばれる
  • 制御変数の値、すなわちクロージャの返値の先頭がnilならばジェネリックfor文は繰り返しを終了する

例1

仕様だけではわかりづらいので、実際にコードで示しましょう。

-- 文字列に含まれる文字を頭から順に返すイテレータ
function my_factory(x)
  local pos = 0
  local length = string.len(x)
  return function()
    pos = pos + 1
    if pos>length then
      return nil
    else
      return string.sub(x,pos,pos)
    end
  end
end

-- 実行例
local str = "abcde"

for c in my_factory(str) do
  print(c)
end

--[[
-- 実行結果
a
b
c
d
e
--]]

念のためここでのクロージャの構成要素と機能を簡単に説明します。

  • ファクトリであるmy_factory()はクロージャを返す。
  • このクロージャにはxとposとlengthという上位値(非ローカル変数とも呼ばれる)と無名関数が含まれています。
  • 上位値はクロージャの中で保持されており、クロージャ実行のたびに上位値が更新されます。
  • 今回の例で言うと、クロージャ実行のたびにposの値が1ずつ増えます。
  • posの値が文字列の長さを超えた時にはnilを返し(終了条件)、それ以外のときはその位置の文字を返します。

このようなクロージャをジェネリックfor文で反復実行する(回す)ことによって、イテレータを実現しています。

なおこれはあくまで一例です。そもそもこの程度の機能ならば普通に数値用for文で回せば良いですよね。

イテレータを自分で作ることの利点は、返値を自由にカスタマイズできる点にあります。

例2

例1はただ一文字を返すだけでしたが、一緒に変数posの値を返したり、あるいは文字を加工して返すことも可能です。

-- 文字列に含まれる文字を頭から順に返すイテレータ(返値複数)
function my_factory2(x, func)
  local pos = 0
  local length = string.len(x)
  return function()
    pos = pos + 1
    if pos>length then
      return nil
    else
      local c = string.sub(x,pos,pos)
      return c, pos, func(c)
    end
  end
end

-- 実行例
local str = "abcde"
local my_func = function(c) return string.upper(c) end

for c1,pos,c2 in my_factory2(str, my_func) do
  print(c1,' ',pos,' ',c2)
end

--[[
-- 実行結果
a 1 A
b 2 B
c 3 C
d 4 D
e 5 E
--]]

例3

さらに面白いのは、仕様さえ満たせば、与えた要素に対して複雑な回し方をしても問題ないという点です。

-- 二次元配列を、横→縦の順に回すイテレータ
function my_factory3(m)
  local col, row = 0,1
  return function()
    col = col + 1

    if col > #(m[row]) then
      col = 1
      row = row + 1
    end

    if row > #m then return nil end

    return col,row, m[row][col]
  end
end

-- 実行例
for col,row,value in my_factory3({{1,2,3},{4,5,6,7}}) do
  print('(',col,',',row,') = ',value)
end

--[[
-- 実行結果
(1,1) = 1
(2,1) = 2
(3,1) = 3
(1,2) = 4
(2,2) = 5
(3,2) = 6
(4,2) = 7
--]]

おわりに

ファクトリを工夫することによって、様々な面白いイテレータを作ることができそうです。

以下は案です。ぜひ挑戦してみてください。

  • 多次元配列の内容を順に表示することができるジェネリックfor文。
  • ファクトリにディレクトリ名を与えることで、そのディレクトリ内の全てのファイル(サブディレクトリ内も含む)を拾い上げるジェネリックfor文。for文の変数(クロージャの返値)は、各ファイル名とそのファイルハンドルとする。

(補足)ここでのクロージャそのものをイテレータと呼ぶことがありますが、実際に反復処理を行っているのはforループであり、クロージャは一連の値を反復処理のために提供しているだけです。そのためこのクロージャーは「ジェネレータ」と呼ぶほうが正確です(とLua公式解説書は述べています)

12
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
13