Logo address

Lua 2 --- Meta Table

2024/10/04

ここでは Lua のメタテーブル(Meta Table)を簡単な応用例に基づいて解説する。

メタテーブルを使うと分数を分数らしく扱うことができる。
例えば

	print(1/2+2/3)

	7/6
を書き出すように構成できる。

数値計算においては分数は実数の一部であり、厳密な扱いができない。メタテーブルを利用すると、分数を実数と区別できて、有理数係数の連立1次方程式の厳密な解が得られる。

Lua のメタテーブルは Python の class 文に相当する。 両者とも既存の演算子を使い、優先順位を保持した上で演算子の解釈を変える。

実行の方法

以下では次のファイルを使う。

見るだけならこのままでもよいが、ダウンロードするなら次がよい。

僕の場合は、テストに使うファイルはすべて
$home/test/
に置いている。どこに置くかは各自の勝手である。
ここに

	f1.lua
	sle.lua
	frac.lua
	num.lua
	gen.lua
を置く。

num.luagen.lua および frac.luaf1.luasle.lua から呼び出されるライブラリである。(すべての内容が使われる訳ではない)

必要な修正

f1.lua および sle.lua は Lua で書かれた実行ファイルである。
f1.lua は分数ライブラリ frac.lua の使い方を示す簡単なデモ。
sle.luafrac.lua の応用例として、整数係数の連立一次方程式を扱っている。

Lua の実行ファイルの先頭の第一行目は

	#!/bin/lua
になっているが、これは Plan9 に合わせられている。
unix系OSの場合には、この部分は
	#!/usr/bin/env lua
に変更すればよいはず

ダウンロードした f1.lua の内容は

#!/bin/lua
-- code below shows usage example for fraction library "frac.lua".
Fr=require("frac").Fr
x=Fr.new(2,3)
y=Fr.new(3,5)
print("with x y",x,y)
print("__add x+y",x+y)
print("__add 1+y",1+y)
print("__add x+1",x+1)
print("__sub x-y",x-y)
print("__sub 1-y",1-y)
print("__sub x-1",x-1)
print("__mul x*y",x*y)
print("__mul 1*y",1*y)
print("__mul x*1",x*1)
print("__div x/y",x/y)
print("__div 1/x",1/x)
print("__div x/1",x/1)
print("__unm -x",-x)
print("__eq x==y", x==y)
print("__eq x==y", x==x)
print("__lt x<y",x<y)
print("__lt x>y",x>y)
print("__le x<=y",x<=y)
print("__le x>=y",x>=y)
print("x[1],x[2]",x[1],x[2])
となっているはずである。

実行すれば

with x y	2/3	3/5
__add x+y	19/15
__add 1+y	8/5
__add x+1	5/3
__sub x-y	1/15
__sub 1-y	2/5
__sub x-1	-1/3
__mul x*y	2/5
__mul 1*y	3/5
__mul x*1	2/3
__div x/y	10/9
__div 1/x	3/2
__div x/1	2/3
__unm -x	-2/3
__eq x==y	false
__eq x==y	true
__lt x<y	false
__lt x>y	true
__le x<=y	false
__le x>=y	true
x[1],x[2]	2	3
を得る。これは次のように読む。
	x~2/3; y=3/5
の下で
行頭の "__add" は計算に使われた metamethod の名称である。"__sub" などについても同様である。
Lua 5.4 で定義された metamethod は次のセクションで示す。

Metatables and Metamethods from Lua 5.4 Manual

Metamethods

Manual からの抜書

__add: the addition (+) operation. If any operand for an addition is not a number, Lua will try to call a metamethod. It starts by checking the first operand (even if it is a number); if that operand does not define a metamethod for __add, then Lua will check the second operand. If Lua can find a metamethod, it calls the metamethod with the two operands as arguments, and the result of the call (adjusted to one value) is the result of the operation. Otherwise, if no metamethod is found, Lua raises an error.
__sub: the subtraction (-) operation. Behavior similar to the addition operation.
__mul: the multiplication (*) operation. Behavior similar to the addition operation.
__div: the division (/) operation. Behavior similar to the addition operation.
__mod: the modulo (%) operation. Behavior similar to the addition operation.
__pow: the exponentiation (^) operation. Behavior similar to the addition operation.
__unm: the negation (unary -) operation. Behavior similar to the addition operation.
__idiv: the floor division (//) operation. Behavior similar to the addition operation.
__band: the bitwise AND (&) operation. Behavior similar to the addition operation, except that Lua will try a metamethod if any operand is neither an integer nor a float coercible to an integer (see §3.4.3).
__bor: the bitwise OR (|) operation. Behavior similar to the bitwise AND operation.
__bxor: the bitwise exclusive OR (binary ~) operation. Behavior similar to the bitwise AND operation.
__bnot: the bitwise NOT (unary ~) operation. Behavior similar to the bitwise AND operation.
__shl: the bitwise left shift (<<) operation. Behavior similar to the bitwise AND operation.
__shr: the bitwise right shift (>>) operation. Behavior similar to the bitwise AND operation.
__concat: the concatenation (..) operation. Behavior similar to the addition operation, except that Lua will try a metamethod if any operand is neither a string nor a number (which is always coercible to a string).
__len: the length (#) operation. If the object is not a string, Lua will try its metamethod. If there is a metamethod, Lua calls it with the object as argument, and the result of the call (always adjusted to one value) is the result of the operation. If there is no metamethod but the object is a table, then Lua uses the table length operation (see §3.4.7). Otherwise, Lua raises an error.
__eq: the equal (==) operation. Behavior similar to the addition operation, except that Lua will try a metamethod only when the values being compared are either both tables or both full userdata and they are not primitively equal. The result of the call is always converted to a boolean.
__lt: the less than (<) operation. Behavior similar to the addition operation, except that Lua will try a metamethod only when the values being compared are neither both numbers nor both strings. Moreover, the result of the call is always converted to a boolean.
__le: the less equal (<=) operation. Behavior similar to the less than operation.
__index: The indexing access operation table[key]. This event happens when table is not a table or when key is not present in table. The metavalue is looked up in the metatable of table.
The metavalue for this event can be either a function, a table, or any value with an __index metavalue. If it is a function, it is called with table and key as arguments, and the result of the call (adjusted to one value) is the result of the operation. Otherwise, the final result is the result of indexing this metavalue with key. This indexing is regular, not raw, and therefore can trigger another __index metavalue.

__newindex: The indexing assignment table[key] = value. Like the index event, this event happens when table is not a table or when key is not present in table. The metavalue is looked up in the metatable of table.
Like with indexing, the metavalue for this event can be either a function, a table, or any value with an __newindex metavalue. If it is a function, it is called with table, key, and value as arguments. Otherwise, Lua repeats the indexing assignment over this metavalue with the same key and value. This assignment is regular, not raw, and therefore can trigger another __newindex metavalue.

Whenever a __newindex metavalue is invoked, Lua does not perform the primitive assignment. If needed, the metamethod itself can call rawset to do the assignment.

__call: The call operation func(args). This event happens when Lua tries to call a non-function value (that is, func is not a function). The metamethod is looked up in func. If present, the metamethod is called with func as its first argument, followed by the arguments of the original call (args). All results of the call are the results of the operation. This is the only metamethod that allows multiple results.

Metatable

Lua のプログラム f1.lua では fraction ライブラリ frac.lua を使って

Fr=require("frac").Fr
x=Fr.new(2,3)
y=Fr.new(3,5)
で分数を生成している。
この場合は
	x=2/3; y=3/5
に相当する作業がここで行われている。
分数は2つの整数(分子、分母)の組である。ここではテーブルを使って
	x={2,3}; y={3,5}
が実行されている。しかし、このままでは xy もテーブルであって、分数ではない。
分数であるためには分数らしい演算が定義されている必要がある。さらに欲を言えば、分数らしく print 関数で表示されたい。
Lua の場合、そのためには metatable を持ち、そこに必要な情報が定義されている必要がある。
ここでは分数のための metatable を mt としている。これを簡単に参照できるように x を表すテーブルに要素 "mt" を持たせ、ここに利用されている metatable へのパス(今の場合 mt)を保存しておくことにする。
local function setmt(t,mt)
	-- set meta-table
	if mt then
		t.mt = mt
		setmetatable(t,mt)
	else
		if t.mt then
			setmetatable(t,t.mt)
		end
	end
end

function Fr.new(a,b) -- function in Fr
	if b==nil then b=1 end
	local f={a,b}
	setmt(f,mt)
	return f
end

frac.lua の中の関数 fr.add(x,y) が分数 x,y の計算方法を定義している。この関数が metatable の加算と結びついていることは

	mt.__add = fr_add
の行によって示されている。

同様に関数 Fr.tostring(x) が分数 x の文字列表現を定義しており、

	mt.__tostring = Fr.tostring
の行が print 関数における分数表示を可能にしているのである。

整数係数連立一次方程式 sle.lua

ここでは次の連立方程式を、先の分数ライブラリ frac.lua を使って解く。

5x + 6y - 7z = 8
2x - 3y + 5z = 1
6x + 7y - 2z = 3

整数係数に限ったのは、簡単のため。

コンピュータを使った連立方程式の数値解は大抵の本に載っているだろう。
正しい答えは、手計算で算出した

	x=22/15, y=-17/15, z=-16/15
である。しかしコンピュータによる数値解は
	x=1.4666666666667, y=-1.1333333333333, z=-1.0666666666667
のような、実数変数による近似解を出すだろう。
これは、数学を教える先生の立場として困る。
sle.lua において、関数 SLE(a,b) が解を与える。
この例の場合、a
	5, 6, -7
	2, -3, 5
	6, 7, -2
を表す 3x3 行列、b
	8
	1
	3
を表す 3x1 行列である。
式の変形によって、左辺を上三角行列に持っていく。その際 a[1][1]0 ではない(このケースでは 5)ことが暗黙に仮定されている。そして最後に上三角行列で表された連立一次方程式を解く。
この方法は結局ガウスの消去法に他ならない。

関数 SLE(a,b) のコードは、実数として求める場合も分数として求める場合も全く同じであることに注意されたい。違いは係数の与え方だけである。

sle.lua の中の関数 test1() では

	a = {}
	a[1] = {5,6,-7}
	a[2] = {2,-3,5}
	a[3] = {6,7,-2}
	b = {8,1,3}
関数 test2() では
	a = {}
	a[1] = i2frac(5,6,-7)
	a[2] = i2frac(2,-3,5)
	a[3] = i2frac(6,7,-2)
	b = i2frac(8,1,3)
ab が与えられている。
test1() が実数として求める場合の係数の与え方、test2() は分数の場合の与え方である。分数の場合の与え方は複雑になっているが、この問題は改善の余地があるかも知れない。ここではコードのシンプルさを優先して、このような書き方になっている。

sle.lua の最後で

	test2()
が実行されている。test1() を試したい場合には、ここを書き換える必要がある。