Logo address

Lua

2009/01/29
2009/02/15 追加
2009/02/16 追加
2009/03/17 追加
2009/04/04 追加
2009/04/05 追加
2012/05/01 追加
2012/05/02 改訂

2024/10/04 追加 Lua 2 -- Metatable

今年の冬休みは Lua で WebDAV のスクリプトを書く事にした。現在は Yuno さんによる Perl によるスクリプトの改造版を使っているものの、実は僕は Perl は吐き気がするほと嫌いだ。あのグロテスクな言語はとてもプログラムに使えたものではない。そこで Lua で書き直すことにしたのだ。

Perl を Lua で置き換えると大きなメリットがある。それらを挙げると:

つまりこれだけのメリットが期待できれば、面倒だけど書き換えたくなるのだ。

なぜ Python ではなく Lua を?
Python は太り過ぎているが Lua は小さい。それに Lua は僕にとって初めての言語だから...

Lua の基礎

Lua は実行速度において評判の良いプログラミング言語である。言語仕様は欲張ってなく、処理系も小さい。

何故 Lua を? Lua は僕にとって今回が初めての言語である。ついでに Lua の感触を掴もうと言う訳だ。あまり慣れていないプログラミング言語を使って何かをやると行き詰まる事が多い。必要な機能が無い事に気付くのだ。Lua は大丈夫が? 「大丈夫だ」と Lua に関しては自信を持って言える。C とのインターフェースが極めて良いからだ。「不足するものがあれば C を使え」、これが Lua の設計者の思想である。実際 Lua は OS インターフェースを殆どサポートしていない。結局自分で組み込まなくてはならないが、難しくはない。Python の C インターフェースに比べると遥かに良くできている。なんと言っても C インターフェース関数をも含めて Lua の仕様としてドキュメント化されているのが嬉しい。

Lua v.s. Python

Lua と Python を比較してみよう。お店に例えると、Python はデパートであるが Lua は専門店である。車に例えると Python は 乗用車であるが Lua はオートバイである。整数のサポートに二つの言語の姿勢がよく現れている。Python はメモリの許す限りの、ものすごく長い整数の演算をサポートしている。他方 Lua においては整数と実数の区別すらしていない。Python にはビット演算が存在するが Lua にはもちろん存在しない*

注*: Lua 5.2 でサポートされた。(2012/05/02)


もっとも、Lua を使って何かシステムソフトを作ろうとすると、整数がちゃんとサポートされていないのがつらい。自分で工夫しなくてはならないのだ。整数のサポートをしてくれれば Lua の使い道はもっと広がるであろうに...

Lua は初心者向けの言語か? No!

初歩的なプログラムであればどんな言語を使っても大差はない。しかしちょっと高度なプログラム、例えばシステムプログラム、になると Lua の場合には不足するものがすぐに現れる。この場合には Lua では C のライブラリを組み込む必要があるのだ。従って Lua は C のエキスパートが使う言語だと言える。

Lua 初心者の戸惑い

文字列の結合演算子

Lua では文字列の結合演算子が '+' ではなく、'..' である。なぜ '+' にはしなかったかと言えば、Lua では数字と、その文字列表現との区別が曖昧だからだ。
	"1" + "2"
は Python なら "12", Lua なら "3" である。Lua では明確に数字に変換したい時には tonumber() なる関数が準備されているが、しかし何となく状況に応じてうまく数字に変換されている。しかし僕は厳格な Python 流が好きだ。

関数の戻り値

関数は値を返す。値を返せない場合にはエラーである。この場合には Lua では nil を返す。
値を返さない関数(いわゆる手続き)の場合には戻り値をどのように構成すべきか?
僕は最初にこの問題を自己流にやっていた。やがて Lua はこの問題に関して明快な方針を持っている事に気付いて、僕のプログラムの中に現れる関数の戻り値を全て書き換えた。

Lua の方針: 関数は、処理結果と、ステータスを返すべきである。

nil と None

Lua の nil と Python の None を比較する。変数へのこれらの値の代入
	x = nil		# Lua

	y = None	# Python
も形の上では良く似ているが、その果たしている役割は全く違う。
Python の
	y = None
は y に値が定義されていないのではなく、いかなる演算の対象にもならない特殊な値 None が定義されているのである。
他方 Lua の nil はもっと意味が強い。
	x = nil
は Python の
	del x		# 名前の削除
に相当する。

Lua では未定義変数は nil の値をとる。他方 Python では未定義変数を参照するとエラーになる。
Lua のはルーズなやり方である。表面上は Lua の方が便利ではあるが、変数のスペルミスなどの実行時のエラーは Python の方が早く取れる。例えば代入文

	x = foo
の場合、Lua では foo が定義されていなくてもエラーにならないが、これは僕にとってちょっと辛い。

部分文字列

s を文字列とすると、s の部分文字列は Lua では
	string.sub(s,2,5)
のように煩わしい記述が要求される。同じ事は Python では簡潔に
	s[1:5]
と書ける*。

注*: Lua のインデックスは 1 から始まるが Python は 0 からである。取り出されるインデックスの範囲は、この場合には
Lua: 2 ≤ i ≤ 5
Python: 1 ≤ i < 5
と考え方が違う。

部分文字列はプログラミング時に頻繁に現れるので、Lua の書き方はうんざりする*

注*: 当時は気付かなかったが、オブジェクト風の s:sub(2,5) の書き方が可能である。Lua 5.1 から可能になったらしい。(2012/05/01 追記)

Lua も Python 流を採用した場合に言語設計の他の部分と矛盾するかと言えば、そうでもないらしい。それどころか文字列 s に対して

	print(s[2])
を実行するとエラーにならず nil を書き出すのである。(これはバグだと思う)

Lua を設計した TeCGraf は意外と頑固なんだと思う。「 [ ] の記法は table に対して適用されるのであって、文字列は table ではない」と誘惑を振り切ったんだろうね。君ならどうする?

正規表現

Lua では正規表現のエスケープ文字は '\' ではなく '%' である。これは巧く出来ている。文字列のエスケーブには C の伝統を引き継いで、多くのプログラミング言語では '\' を採用している。この点では Lua も Python も変わらない。そこで文字列の中に正規表現が入り込むと、気が狂う程頭の中が混乱することになる。Python はこの問題を
	R"...."
のように文字列の前に R を添える事によって処理したが、Lua の方がスマートだ。

Lua では何故か '|' がサポートされていない。これをサポートした場合には何かネガティブな問題をもたらすので止めたのであろう。どうしても '|' が必要であれば Lua は採用できないことになる。もっとも WebDAV サーバー程度のプログラム(相当複雑な部類に属する)では '|' は必要はなかった。

module

2009/02/15

モジュールの作成は次の例で分かる。
ファイル: foo.lua

print("module foo is being loaded")
module("foo",package.seeall)
function f()
  print("OK")
end
print("module foo is loaded")
ここでは関数 f() をモジュール名 foo でモジュール化している。最初の print はモジュールの外での実行、残りの print はモジュールの中での実行である。

モジュール foo を呼び出すには:
ファイル: a.lua

#!/usr/local/bin/lua
print(package.path)
require("foo")
foo.f()
ここで package.path はモジュールが置かれているパスであり、default は以下の実行結果で判明するが、自分で設定してもよい。

実行結果は

-bash$ a.lua
./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua
module foo is being loaded
module foo is loaded
OK

さて分からないのは module の第二引数(オプション)の package.seeall である。これによって module foo の中でグローバル変数が使えるようになる。逆にこれが無ければ、モジュールの中では事実上何も出来ない。そして module の第二引数で可能なのは(現在のところ)事実上 package.seeall だけのようである。(こんな事分かんないよねー!)

何故第二引数の default を package.seeall にしなかったのか?

module の第二引数がどのように使われるのかを見てみよう。

print("module foo is being loaded")
function g()
  print("g:OK")
  package.seeall(foo)
end
module("foo",g)
function f()
  print("f:OK")
end
print("module foo is loaded")

a.lua は同じである。実行結果は

-bash$ a.lua
./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua
module foo is being loaded
g:OK
module foo is loaded
f:OK
module の第二引数に package.seeall 以外の関数を与えた場合に、どのように役に立つのだろうか ?
あるいは第二引数は将来の含みであって、次のバージョンでは package.seeall 以外の関数がサポートされるのかも知れない。

なおモジュールパスの ? は shell の * と似ている。例えば lib?.lua と書けば、ファイル libfoo.lua にジュール foo を作る事になろう。僕なら ? ではなく、率直に * の仕様にするのに...

2012/05/01

関数 module に関して疑問を述べたが、久しぶりに http://www.lua.org/ にアクセスして見ると Lua 5.2 がリリースされていた。マニュアルを読むと、関数 module は廃止すると言う。(実際には互換性のために残されているが、使わない方が良い)
理由は、関数 module が無くても問題はないと言う事である。この件に関しては、「モジュールと 関数 require」で取り上げる。

long comment

2009/02/16

Lua では長いコメントは

--[[
...
...
--]]
</pre>
のように書く。"{...}" の部分は何でもよい。この中に "{--]]}" が含まれるなら
<pre class="code">
--[=[
...
...
--]=]
と書けばよい。"=" の個数は実はいくつでもよい。

僕はこのような Lua の仕様に最初は違和感を覚えたが、実に良く出来ている事に気付いた。(気付いたのは今日ではなくもっと以前である。今頃とは言わないで...)
だってえ...

-- [[
...
...
--]]
のように "-" と "[" の間に空白を一個入れるだけでコメントが外れるからだ。

local

2009/03/17

最初に Lua に接した時から僕は Lua の local 修飾子に違和感を覚えた。そして今もなお "?" だ。
Lua はなぜ Javascript や他の言語のように var による変数宣言を採用しなかったのだろうか?

言葉の響きから var は変数宣言であるが、local は一般的な修飾子である。修飾子としての local が意味を持ちそうなものに function への local 修飾がある。

local function f()
  print("f")
end
f()
これはエラーになる*。最後の f() はグローバルな名前の呼び出しだからである。うーん、local は C の static とも違うんだね...

注*: 会話モードではエラーになるが、ファイルに書いて実行させればエラーにはならない。ようやく分かった、module の中ではエラーにならない理由が... (2009/04/04)

修飾子 local は module の中では意味を持っている。

module("foo",package.seeall)
local function f()
  print("f")
end
function g()
  print("g")
end
g()
f()
これを foo.lua に保存して
-bash$ lua
Lua 5.1.2  Copyright (C) 1994-2007 Lua.org, PUC-Rio
> require("foo")
g
f
> foo.g()
g
> foo.f()
stdin:1: attempt to call field 'f' (a nil value)
stack traceback:
	stdin:1: in main chunk
	[C]: ?
>
つまり local の修飾子は module を使った時に合理的な(つまり local の響き通りの)振る舞いをしている。

関数の中での関数の定義はどうか?

function f()
  function g()
    print("g")
  end
  print("f")
end
g()
f()
これを実行すると
lua: bar.lua:7: attempt to call global 'g' (a nil value)
stack traceback:
	bar.lua:7: in main chunk
	[C]: ?
これは当然。関数 f() の中で定義された g() は外からは見えないのだからと思ったら間違いである。
function f()
  function g()
    print("g")
  end
  print("f")
end
f()
g()
を実行するとエラーにならない。出力は
f
g
エラーにならなかったのは、f() が実行されることによって g がグローバルな名前空間(Lua では _G)に持ち込まれるからだ。

そこで今度は

function f()
  local function g()
    print("g")
  end
  print("f")
end
f()
g()
を実行してみる。すると
f
lua: bar.lua:8: attempt to call global 'g' (a nil value)
stack traceback:
	bar.lua:8: in main chunk
	[C]: ?
つまり local の役割はグローバルな名前空間に名前が入るのをブロックしているのだ。

しかし、何と言うシンプルな割り切り方だ! このシンプルさによって、Lua の実装は簡単になったであろう。その代償は、直感の犠牲である。宣言の構造が名前空間を決めるのではなく、実行の流れが名前空間を決めているのであるから直ぐには分からない...

Lua 雑感

import

2009/04/04

注意: この内容は Lua 5.2 では正しくない。次の節を見よ。(2012/05/02)

import は Python でモジュールを使う時の宣言子である。モジュール foo を使う時には

	import foo
Lua の場合にはこれに相当するものとして require() がある。但し 宣言子ではなく、関数である。
	require("foo")
しかし Python の場合には
	from foo import *
のように、モジュール foo から全ての名前を取り込める。また * の部分に
	from foo import bar,baz
のように、取り込みたい名前を列挙して選択的にも取り込める。これに相当する Lua の関数は何か? 存在しない。この問題に対して、多分 Python のユーザからの声だと思うが、ネット上にも質問が見られるが、適切な回答がないようだ。

Lua の場合には、もしも欲しいなら、自分で作らなくてはならない。次のコードが参考になるだろう。

function import(t,...)
  local u = {...}
  if u[1] == "*" then
    for k,v in pairs(t) do
      _G[k] =  v
    end
  else
    for k,v in ipairs(u) do
      _G[v] = t[v]
    end
  end
end

これは次のように実行する。

t = require("foo")
import(t,"*")

モジュール foo の中の、bar, baz を取り込みたいなら

t = require("foo")
import(t,"bar","baz")
とやればよい。require()import() の二つを使うのが嫌なら、import() を一つに纏めた仕様にすることもできるだろう。

しかし、僕は import() は要らないと思う。代わりに

require("foo")
bar = foo.bar
baz = foo.baz
とやればよいだけだから。(名前を変えて取り込む事もできる。Python でも同じだけどもね...)

モジュールと関数 require

2012/05/01
2012/05/02 改訂
2012/05/03 追加

モジュールの書き方

Lua 5.2 のマニュアルには、モジュールの中に使われる関数 module の廃止が予告されている。この関数は、モジュールの中で定義されている関数が、モジュールを呼び出した際に、そのまま名前空間に入って行くのを防いでいる。例えば

function foo()
  print("This is foo")
end
function bar(s)
  print(s)
end

図1. module hoge.lua
を呼び出すプログラム

require("hoge")
foo()
bar("This is bar")
は、次の出力を出す。

This is foo
This is bar

つまり、require("hoge") は Python の

	from hoge import *
と同じように働き、hoge の全ての名前を取り込む。これは、名前の衝突を引き起こす可能性があり、多くのモジュールを持ち出すと、そのリスクが増大する。
Lua 5.1 でサポートされていた、モジュールの中の module 関数は、仕様に変な部分があるが、その問題に対する1つの回答だったのである。

では、どうしたら良いか? Lua 5.2 のマニュアルには、Lua の一般的な方法の中で問題を解決できるのだと言う。(しかし具体的な解決策は示されていない)

次のようなのはどうか?

local function foo()
  print("This is foo")
end
local function bar(s)
  print(s)
end
hoge={}
hoge.foo=foo
hoge.bar=bar

図2. module hoge.lua

このように書けば、グローバルな名前空間に取り込まれるのは hoge だけである。

require("hoge")
hoge.foo()
hoge.bar("This is bar")

結果は

This is foo
This is bar

となる。つまり Python の

	import hoge
相当の事が可能になるのであるが、僕は
	import hoge as fuga
のように使えたら良いと思う。つまりエンドユーザは、名前をグローバルな空間に持ち込む事にもっと臆病になるべきであって、hoge だって衝突するかも知れないから、代替え手段が欲しいのである。

理想的なのは、require が、モジュールの中で定義されている名前テーブルを返してくれることである。どうすれば良いか? 筆者は今日になって、ようやく方法が存在する事が分かった。モジュールの最後の return で解決するのだ。

local function foo()
  print("This is foo")
end
local function bar(s)
  print(s)
end
local t={}
t.foo=foo
t.bar=bar
return t

図3. module hoge.lua
その下で
fuga = require("hoge")
fuga.foo()
fuga.bar("This is bar")
のように実行するのである。


注意: require("hoge") を2度実行しても、二重にはロード(実行)されない。Lua にはモジュールの二重実行を防ぐための仕組みがある。hoge が汎用的なモジュールであれば、複数のモジュールから hoge が呼び出されるであろう。hoge がデータを持つ場合には、そのデータも共有される。それが問題をもたらすようであれば、closure を使って、データを独立させる必要がある。

書き方の例: inspecttab

-- Lua Tools
-- coded by Kenar (Kenji Arisawa)
-- email: arisawa@aichi-u.ac.jp

local function printf(fmt,...)
  io.write(string.format(fmt,...))
end

local function dumpstr(s)
  if type(s)=="string" then
    local repl={["\n"]="\\n",["\t"]="\\t"}
    return s:gsub("(%c)",repl)
  end
  return s
end

local spectype={}
spectype["string"]=true
spectype["number"]=true
spectype["boolean"]=true

local function inspecttab(t,n,done)
  if n==nil then
    done={}
    n=""
  end
  if t==nil then
    return
  end
  done[t]=true
  for k,v in pairs(t) do
    if type(v) == "table" then
      if done[v] then
        printf("%s%s\t%s # done\n",n,k,v)
      else
        printf("%s%s\t%s\n",n,k,v)
        inspecttab(v,n.."\t",done)
      end
    elseif spectype[type(v)] then
      printf("%s%s\t%s: %s\n",n,k,type(v),dumpstr(v))
    else
      printf("%s%s\t%s\n",n,k,v)
    end
  end
end

local t={
  printf=printf,
  inspecttab=inspecttab
}

return t
図4. ファィル名:tools.lua

実行例を示す。手軽に利用できる一番大きなテーブルは _G である。_G のテーブルは、参照関係が循環しているので、注意してコードを書かないと、無限ループにハマる。図4のプログラムでは、一度書き出したテーブルは、再度書かないことで、この無限ループを避けている。「# done」と表示されているテーブルは、既出のテーブルなのである。

ソートした方が良かったかも... (iPad だと全部が見えない...)

Lua XML

Lua は XML の処理に使いやすい言語だと思う。Lua で扱うデータ構造は十分に強力だし、正規表現による文字列の操作も扱いやすい。Python の場合には正規表現を使う前にはコンパイルしなくてはならないので面倒であるが、Lua はその必要はないのはありがたい。(2009/04/04)

XML collect

Lua による XML をネットで調べたら次のコードが http://lua-users.org/wiki/LuaXml に載っていた。

    function parseargs(s)
      local arg = {}
      string.gsub(s, "(%w+)=([\"'])(.-)%2", function (w, _, a)
        arg[w] = a
      end)
      return arg
    end

    function collect(s)
      local stack = {}
      local top = {}
      table.insert(stack, top)
      local ni,c,label,xarg, empty
      local i, j = 1, 1
      while true do
        ni,j,c,label,xarg, empty = string.find(s, "<(%/?)(%w+)(.-)(%/?)>", i)
        if not ni then break end
        local text = string.sub(s, i, ni-1)
        if not string.find(text, "^%s*$") then
          table.insert(top, text)
        end
        if empty == "/" then  -- empty element tag
          table.insert(top, {label=label, xarg=parseargs(xarg), empty=1})
        elseif c == "" then   -- start tag
          top = {label=label, xarg=parseargs(xarg)}
          table.insert(stack, top)   -- new level
        else  -- end tag
          local toclose = table.remove(stack)  -- remove top
          top = stack[#stack]
          if #stack < 1 then
            error("nothing to close with "..label)
          end
          if toclose.label ~= label then
            error("trying to close "..toclose.label.." with "..label)
          end
          table.insert(top, toclose)
        end
        i = j+1
      end
      local text = string.sub(s, i)
      if not string.find(text, "^%s*$") then
        table.insert(stack[#stack], text)
      end
      if #stack > 1 then
        error("unclosed "..stack[stack.n].label)
      end
      return stack[1]
    end
次にこのコードの実行例を載せる。
content = [[
<?xml version="1.0" encoding="utf-8"?>
<D:lockinfo xmlns:D="DAV:">
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
<D:owner>
<D:href>http://www.apple.com/webdav_fs/</D:href>
</D:owner>
</D:lockinfo>
]]

s = collect(content)
print(s[1])
print(s[2].label, s[2].xarg)
t = s[2].xarg
for k,v in pairs(t) do print(k,v) end
この結果を次に示すが明らかにおかしい。
<?xml version="1.0" encoding="utf-8"?>

D	table: 0x6F488
D	DAV:
どうやら問題のプログラムコードは名前ラベルが存在しない事を前提にしているのだ。それにしても何で "label" を意識したコードが含まれているのだ?

XML collect (修正版)

XML のタグは

	<NAME ATTRS>
	<NAME ATTRS/>
	</NAME>
の3つに類型される。ここに NAME
	D:lockinfo
のように、一般的に言えばラベルがついた名前、ATTRS
	xmlns:D="DAV:"
のような名前の属性を示す部分である。この例では属性は1個であるが、存在しなくても良いし、逆に複数の属性の並びが含まれても構わない。
XML の規則では NAME のシンタックスは単純である。

また ATTRS は空白で区切られた ATTR のリストである。ここでのシンタックス規則は単に

だけである。

次のプログラムの関数名は

	xml.collect
となっている。このように "." を含む名前付けを可能にするためには
	xml = {}
が前もって実行されている必要がある。

function xml.collect(s)
  local ns
  local stack = {}
  local top = {}
  local ni,c,name,attrs, empty
  local i,j,decl =string.find(s, "<%?xml *(.-)%?>")

  table.insert(stack, top)
  j = j + 1
  i = j
  while true do
    -- tags have the form: "<NAME ATTRS/?>", "<NAME/?>", "</NAME>",
    ni,j,c,name,attrs, empty = string.find(s, "<(/?)([^%s/<>]+) *(.-)(/?)>", i)
    -- print("DBG0: ",ni,j,c,"/",name,attrs,"/",empty)
    if not ni then break end
    local text = string.sub(s, i, ni-1)
    if not string.find(text, "^%s*$") then
      table.insert(top, text)
    end
    if empty == "/" then  -- empty element tag
      local e
      e = {name=name, attrs=attrs}
      table.insert(top,e)
    elseif c == "" then   -- start tag
      top = {name=name, attrs=attrs}
      table.insert(stack, top)   -- new level
    else  -- end tag
      local toclose = table.remove(stack)  -- remove top
      top = stack[#stack]
      if #stack < 1 then
        error("nothing to close with "..name)
      end
      if toclose.name ~= name then
        error("trying to close "..toclose.name.." with "..name)
      end
      table.insert(top, toclose)
    end
    i = j+1
  end
  local text = string.sub(s, i)
  if not string.find(text, "^%s*$") then
    table.insert(stack[#stack], text)
  end
  if #stack > 1 then
    error("unclosed "..stack[stack.n].name)
  end
  return stack[1]
end
このコードに現れる正規表現であるが
	string.find(s, "<(/?)([^%s/<>]+) *(.-)(/?)>", i)

	(.-)
は Lua では最小マッチングを意味するが、あまりポピュラーな表記ではない。この場面では率直に
	([^/<>]*)
と置き換えても良いはずである。

次のプログラムは xml.collect が集めた結果を分析するのに役に立つ。

function xml.printstack(t, Ntab)
  -- this function shows the structure of stack returned from xml.collect()
  -- please call me by: t = xml.collect(content); xml.printstack(t)
  if t == nil then
    return
  end
  if Ntab == nil then
    Ntab = 0
  end
  if type(t) == "string" then
    print(Ntab,'T', t)   -- text outside of tags
    return
  end
  Ntab = Ntab + 1
  -- Ntab: tag level (namespace level)
  -- #t: number of element, starting tag (if #t>0) or empty tag (if #t == 0)
  -- t.name: tag name; t.attrs: tag attributes
  print(Ntab,#t,t.name, t.attrs)

  for i=1, #t do
    xml.printstack(t[i], Ntab)
  end

  print(Ntab,"/",t.name)
  Ntab = Ntab - 1
end
実行例は
content = [[
<?xml version="1.0" encoding="utf-8"?>
<D:lockinfo xmlns:D="DAV:">
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
<D:owner>
<D:href>http://www.apple.com/webdav_fs/</D:href>
</D:owner>
</D:lockinfo>
]]

s = xml.collect(content)

print(s[1].name)
print(s[1][1].name)
print(s[1][1][1].name)

xml.printstack(s[1])
この結果は

term% lua libxml.lua
D:lockinfo
D:lockscope
D:exclusive
1	3	D:lockinfo	xmlns:D="DAV:"
2	1	D:lockscope
3	0	D:exclusive
3	/	D:exclusive
2	/	D:lockscope
2	1	D:locktype
3	0	D:write
3	/	D:write
2	/	D:locktype
2	1	D:owner
3	1	D:href
3	T	http://www.apple.com/webdav_fs/
3	/	D:href
2	/	D:owner
1	/	D:lockinfo
term%
読者は xml.printstack のコードを利用して、xml.collect の結果を必要に応じて様々に加工する関数を書く事が事ができる。


実際に collect()を使ってみると、分かり難さを感じる。諸悪の根源は collect() が情報を十分に組織化していないことにある。例えば nodecollect() から得られたノードの一つとする。そのときに node[1] が表しているものは、type(node[1])string ならそのノードのテキストデータ、table なら子供のノードである。このような非一貫性が collect() の使い心地を酷く損なっている。次に示す mktree() はこの問題を解決している。(2009/04/04)

XML mktree

2009/04/04

さて僕は最近、現実に現れるある複雑な XML データ(XBRL のデータ)を処理していて、強力なツールを必要とした。そのために開発したのが次の mktree() である。この関数は気に入っているので紹介しておく。

XML のノードは、名前(tag name)と属性(tag attributes)とノードに属するテキストデータを持っている。さらに一般的に言えば、親と子供たちと繋がっている。

このような名前空間のモデルは強力ではあるが、XML で扱われる名前空間の一般的なモデルではない。例えば次の例
	<definition>The <term>computer</term> is blah blah ... </definition>
を見れば分かるように、XML による文章の持つ名前空間は、ここで扱っている名前空間のモデルの域を超えている。(2009/04/05)

ノードを N とすると、

	N.name	# node name
	N.attrs	# node attributes
	N.attr	# elements of the attributes
	N.data	# value of the node
	N.parent	# parent node
	N.no	# array index of this node referenced by the parent
	N[1]	# node of the child 1
	N[2]	# node of the child 2
	...
	N[n]	# node of the child n
でそれらを参照できれば便利である*。ここで n#N に等しい。また
	<comment id="X0123" name="alice" age="18">Hello everyone, my name is alice.</comment>
のようなノードがあれば N.name は "comment" であり、N.attrs
	id="X0123" name="alice" age="18"
である。さらに N.data
	Hello everyone, my name is alice.
である。

注*: これと良く似た名前空間の構造はファイルシステムに見られるが、ファイルシステムの名前空間は幾分制約が強い。すなわち、data があれば子供は無い。子供があれば、data は無い。
なお Python で i 番目の子供のノードを表現するなら N[i] ではなく
	N.child[i]
となるだろう。Python でもノード N に対して形式的に N[1],N[2],... を使えるけれども、それらは辞書の要素であって、リストではない。あとのデータ処理を考えると好ましい考え方ではない。(実行速度の問題、加工に伴うよけいな複雑さ、子供の個数の管理など) Lua の table は Python で言う辞書とリストを兼ねているのである。

一般的に言えば N.attrs は要素を持っており、それらを簡単に参照したいとするニーズは高い。そこで N.attr を導入した。このケースでは

	N.attr["id"]	# "X0123"
	N.attr["name"]	# "alice"
	N.attr["age"]	# "18"
である。

N.no は親ノードから見た、そのノードの配列番号である。従って

	N.parent[N.no]
はそのノード自身を表しており
	N.parent[N.no + 1]
で次の兄弟のノードに移る事ができる。

さらに DOM 的な機能を実現しやすい情報を追加する。すなわち、ノードの名前(tag name) と、属性に現れる id の一覧のデータベースを返す。つまり XML 仕様の文字列 xmldata をインプットとして

	node,tags,ids = mktree(xmldata)
によって、ルートノード node の他に、タグ名の一覧 tagsid の一覧 ids を返す。一般的に言えば、同じ名前のタグ名は複数存在するので tags は Lua の配列であり、
	for i,v in ipairs(tags["comment"]) do
	  print(v.attr.name,v.attr.age)		# alice 18	-- etc
	  print(v.data)				# Hello everyone, my name is alice.	--- etc
	end
id の場合には、一意に決まるはずだから繰り返し文を使わなくても
	v = ids["X0123"]
	print(v.attr.name,v.attr.age)	# alice	18
	print(v.data)			# Hello everyone, my name is alice
で目的のデータに辿り着く。

match = string.match
find = string.find

local function deco(a)
  -- decompose attrs
  local t = {}
  local s,b,c,d
  local q = "'\""
  if a == nil then
    return nil
  end
  s = split(a)
  for i=1,#s do
    a,b,c,d = match(s[i],"([^=]+)=(["..q.."])([^"..q.."]*)(["..q.."])")
    if a == nil or b ~= d then
      print("## invalid xml attr format: ",s[i])
    else
      t[a] = c
    end
  end
  return t
end

local function putdb(e,names,ids)
  if names[e.name] == nil then
    names[e.name] = {}
  end
  table.insert(names[e.name], e)
  if e.attr and e.attr.id then
    if ids[e.attr.id] then
      error("same id: "..e.attr.id)
    end
    ids[e.attr.id] = e
  end
end

function mktree(s)
  -- modified collect()
  -- mktree() has a DOM like ability
  -- a powerful tool to evaluate xml data
  local ns
  local stack = {}
  local top = {}
  local ni,c,name,attrs, empty
  local i,j,decl =find(s, "<%?xml *(.-)%?>")
  local ids = {} -- id table
  local names = {} -- tag name table
  if j == nil then
    i = 1
  else
    i = j + 1
  end
  while true do
    -- tags have the form: "<NAME ATTRS/?>", "<NAME/?>", "</NAME>",
    -- we must skip comment! we cant't use "repeat ... until" here
    ni,j,c,name,attrs, empty = find(s, "<(/?)([^%s/>]+) *(.-)(/?)>", i)
    while name == "!--" do
      i = j+1
      ni,j,c,name,attrs, empty = find(s, "<(/?)([^%s/>]+) *(.-)(/?)>", i)
    end
    -- here attrs == "" if no attr but we make it nil
    if attrs == "" then
      attrs = nil
    end
    if not ni then
      break
    end
    local text = sub(s, i, ni-1)
    if not find(text, "^%s*$") then
      top.data = text
    end
    if empty == "/" then  -- empty element tag
      local e,attr
      attr = deco(attrs)
      e = {name=name, attrs=attrs, attr=attr, parent=stack[#stack], no=#top+1}
      table.insert(top,e)
      putdb(e,names,ids)
    elseif c == "" then   -- start tag
      local e,attr
      attr = deco(attrs)
      e = {name=name, attrs=attrs, attr=attr, parent=stack[#stack], no=#top+1}
      table.insert(stack, e)   -- new level
      putdb(e,names,ids)
      top = e
    else  -- end tag
      close = table.remove(stack)  -- remove top
      if close.name ~= name then
        error("inconsitent open and close tag: "..close.name.." with "..name)
      end
      if #stack < 1 then
        break
      end
      top = stack[#stack]  -- stack[0] == nil
      table.insert(top, close)
    end
    i = j+1
  end
  if #stack > 1 then
    error("unclosed "..stack[#stack].name)
  end
  return close,names,ids
end

ここに現れる split() は次のように定義されている。

gsub = string.gsub
find = string.find

function split(s,sep)
  local i,j,k
  local t={}
  if sep == nil then
    gsub(s,"([^%s]+)",function(a) table.insert(t,a) end)
    return t
  end
  i = 1
  while true do
    j,k = find(s,sep,i,true)
    if j == nil then
      table.insert(t,sub(s,i))
      break
    end
    table.insert(t,sub(s,i,j-1))
    i = k + 1
  end
  return t
end

注意: デコードは省略されているので、そういう事が必要なデータを扱う場合にはコードを追加しなくてはならない。

以上のコードは自由に利用しても構わないが、出典だけは明示して欲しい。
You can use the codes above freely under the Lua license 5.0 and later versions (http://www.lua.org/license.html)

XML viewer mkidf

2009/04/04

現実に使われる XML のデータは複雑である。例えばビジネスに使われる会計データの仕様(XBRL)が最近から運用されだしている。公開されている企業別のデータを見てみると、実に複雑で大きくて、一つの企業の処理に関係する XML で書かれたファイルは数十個に及ぶ。その中の一つの冒頭部部だけを示すと

<?xml version="1.0" encoding="utf-8"?>
<!-- Edited with XiRUTE .NET Library 01-04 -->
<xbrli:xbrl xmlns:iso4217="http://www.xbrl.org/2003/iso4217" xmlns:jpfr-q3r-E02149-000="http://info.edinet-fsa.go.jp/jp/fr/gaap/E02149-000/q3r/2008-12-31/01/2009-02-12" xmlns:jpfr-t-cte="http://info.edinet-fsa.go.jp/jp/fr/gaap/t/cte/2008-02-01" xmlns:link="http://www.xbrl.org/2003/linkbase" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpfr-di="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/di/2008-02-01" xmlns:xbrli="http://www.xbrl.org/2003/instance">
  <link:schemaRef xlink:type="simple" xlink:href="jpfr-q3r-E02149-000-2008-12-31-01-2009-02-12.xsd" />
  <link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleNotesNumber" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/NotesNumber" />
  <link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfIncomeYTD" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfIncomeYTD" />
  <link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfIncomeQuarter" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/
ConsolidatedQuarterlyStatementsOfIncomeQuarter" />
  <link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleNotesNumberPeriodEnd" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/NotesNumberPeriodEnd" />
  <link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyBalanceSheets" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyBalanceSheets" />
  <link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfCashFlowsIndirect" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfCashFlowsIndirect" />
  <xbrli:context id="CurrentQuarterConsolidatedInstant">
    <xbrli:entity>
      <xbrli:identifier scheme="http://info.edinet-fsa.go.jp">E02149-000</xbrli:identifier>
    </xbrli:entity>
    <xbrli:period>
      <xbrli:instant>2008-12-31</xbrli:instant>
    </xbrli:period>
  </xbrli:context>
....
....
のようなものである。 実際のこのデータ a.xbrl

このようなデータを眺めていると XML データというのは人間にとって、そのデータ構造が分かり難いものだと思う。このケースでは特に属性部の記述量が多くて、見難くしている。もっと分かりやすい表現法がないととても全体像が捉えられないであろう。そこで次のような形式で表示するツールを作った。

# file: jp/fr/gaap/E02149-000/q3r/2008-12-31/01/2009-02-12/jpfr-q3r-E02149-000-2008-12-31-01-2009-02-12.xbrl
# format: idf-1.0
# Edited with XiRUTE .NET Library 01-04
xbrli:xbrl
- xmlns:iso4217="http://www.xbrl.org/2003/iso4217"
- xmlns:jpfr-q3r-E02149-000="http://info.edinet-fsa.go.jp/jp/fr/gaap/E02149-000/q3r/2008-12-31/01/2009-02-12"
- xmlns:jpfr-t-cte="http://info.edinet-fsa.go.jp/jp/fr/gaap/t/cte/2008-02-01"
- xmlns:link="http://www.xbrl.org/2003/linkbase"
- xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:jpfr-di="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/di/2008-02-01"
- xmlns:xbrli="http://www.xbrl.org/2003/instance"
	link:schemaRef
	- xlink:type="simple"
	- xlink:href="jpfr-q3r-E02149-000-2008-12-31-01-2009-02-12.xsd"
	link:roleRef
	- xlink:type="simple"
	- xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleNotesNumber"
	- roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/NotesNumber"
	link:roleRef
	- xlink:type="simple"
	- xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfIncomeYTD"
	- roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfIncomeYTD"
	link:roleRef
	- xlink:type="simple"
	- xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfIncomeQuarter"
	- roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfIncomeQuarter"
	link:roleRef
	- xlink:type="simple"
	- xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleNotesNumberPeriodEnd"
	- roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/NotesNumberPeriodEnd"
	link:roleRef
	- xlink:type="simple"
	- xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyBalanceSheets"
	- roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyBalanceSheets"
	link:roleRef
	- xlink:type="simple"
	- xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfCashFlowsIndirect"
	- roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfCashFlowsIndirect"
	xbrli:context
	- id="CurrentQuarterConsolidatedInstant"
		xbrli:entity
			xbrli:identifier
			- scheme="http://info.edinet-fsa.go.jp"
			* E02149-000
		xbrli:period
			xbrli:instant
			* 2008-12-31
....
....

ここに

サンプルの a.xbrl をこの形式(僕は IDF と読んでいる)に変換したファイルも参考のために見えるようにしておく。a.idf
僕はこれによってようやく XBRL の全体像を捉えられるようになった。

次に示すのは関数 mkidf()mkxml() である。関数 mkidf() は XML ファイルを読み取り、IDF 形式で出力する。関数 mkxml() はその逆を行う。mkxml() ではインデントが指定できるので、この二つの関数の組み合わせで、XML ファイルをインデントを指定して整形できる。なお mkidf() のインデントは TAB 固定である。インデントが命であるから、空白とタブの混合はサポートしない。(Python はプログラムのブロックをインデントで表しているが、そのようなやりかたは、データ表現に対しては重大な問題を引き起こすだろう)

local function mksp(n)
  s = ""
  for i=1,n do
    s = s .. "\t"
  end
  return s
end

function mkidf(file)
  -- file: xml file
  -- print in indented data format(idf)
  -- idf is useful to view logical structure of xml file
  local ni,c,name,attrs, empty
  local stack = {}
  local s,t
  local f
  print("# format: idf-1.0")
  f = io.open(file)
  if f == nil then
    print("no such file: "..file)
    return
  end
  s = f:read("*a")
  f:close()
  local i,j,decl =find(s, "<%?xml *(.-)%?>")
  if j == nil then
    i = 1
  else
    i = j + 1
  end
  while true do
    -- tags have the form: "<NAME ATTRS/?>", "<NAME/?>", "</NAME>",
    ni,j,c,name,attrs, empty = find(s, "<(/?)([^%s/>]+) *(.-)(/?)>", i)
    while name == "!--" do
      -- <!-- comments -->
      print(mksp(#stack).."# "..sub(s,ni + 5,j - 4))
      i = j+1
      ni,j,c,name,attrs, empty = find(s, "<(/?)([^%s/>]+) *(.-)(/?)>", i)
    end
    -- here attrs == "" if no attr but we make it nil
    if attrs == "" then
      attrs = nil
    end
    if not ni then
      break
    end
    local text = sub(s, i, ni-1)
    if not find(text, "^%s*$") then
      print(mksp(#stack - 1).."* "..text)
    end
    if empty == "/" or c == "" then -- empty element tag or starting tag
      print(mksp(#stack)..name)
      if attrs then
        t = split(attrs)
        for i=1,#t do
          print(mksp(#stack).."- "..t[i])
        end
      end
      if empty ~= "/" then   -- start tag
        table.insert(stack,name)
      end
    else  -- end tag
      local toclose = table.remove(stack)  -- remove top
      if toclose ~= name then
        error("trying to close "..toclose.." with "..name)
      end
    end
    i = j+1
  end
end

function mkxml(file,indent)
  -- make xml file from idf file
  -- reverse of mkidf()
  local function sp(n)
    local s = ""
    for i=1,n do
      s = s .. " "
    end
    return s
  end
  local s,t,i,stack,t0,t00
  if indent == nil then
    indent = 0
  end
  stack = {}
  io.write('<?xml version="1.0" encoding="utf-8"?>\n')
  i = -1
  for line in io.lines(file) do
    s = match(line,"^(\t*)")
    t = sub(line,#s + 1)
    if sub(t,1,1) == " " then
      print("\n### use tabs for indent")
      os.exit()
    end
    t0 = sub(t,1,2)
    if t0 == "- " then -- an attribute/value pair
      io.write(" "..sub(t,3))
    elseif t0 == "* " then -- a data value
      if t00 and t0 ~= t00 then
        io.write(">")
      end
      io.write(sub(t,3))
    elseif sub(t,1,2) == "# " then -- comments
      io.write(sp(#stack * indent).."<!-- "..sub(t,3).." -->\n")
    elseif match(t,"^%w") then -- a name
      -- we have 9 cases:
      --   t00 = "- "; t00 = "* "; others
      --   #s > i; #s == i; #s < i
      if #s > i then -- new level, t must be a name
        -- note that t00 ~= "* " here
        if i ~= -1 then
          io.write(">\n")
        end
        table.insert(stack,name)
        if #s ~= #stack then
          print("# invalid indent")
          os.exit()
        end
        io.write(sp(#s * indent).."<"..t)
      else
        if t00 == "* " then
          io.write("</"..name..">\n")
        else
          io.write("/>\n")
        end
        if #s < i then -- back to low level until #s == i
          repeat
            name = table.remove(stack)
            io.write(sp(#stack * indent).."</"..name..">\n")
          until #s == #stack
        end
        io.write(sp(#s * indent).."<"..t)
      end
      name = t
      i = #s
    else
      print("# invalid format")
      os.exit()
    end
    t00 = t0
  end
  if t00 == "* " then
    io.write("</"..name..">\n")
  else
    io.write("/>\n")
  end
  while #stack > 0 do
    name = table.remove(stack)
    io.write(sp(#stack * indent).."</"..name..">\n")
  end
end

関数 mkidfmktree() を使えばもっと簡単に書けるのだが、その場合にはテータを全てメモリーに読み取る事になるので、実行効率の良い mkidf() を新たに書いた。

エンコードやデコードを行っていないので注意する。

以上のコードは自由に利用しても構わないが、出典だけは明示して欲しい。
You can use the codes above freely under the Lua license 5.0 and later versions (http://www.lua.org/license.html)

XML 雑感

2009/04/05

性格の違うものを一緒に議論するのは混乱の元である。XML に関してはこの点はどうか? XML は XBRL のようにコンピュータによるデータ処理のためのものなのか? それとも HTML のように人に読ませるためのドキュメント処理のためのものなのか? あるいはその両方を目指しているのか?

この二つは、共通部分もあるが、基本的に正反対の方向である。
前者の場合には、処理の誤りは致命的である。さらに悪い事にその処理の誤りが見え難い。データ形式については組織化できるし、組織化しなくてはならない。
他方後者の場合には自由が求められる。処理の誤りは直ちに人目に曝され、致命的な結果をもたらす事はない。

一例を挙げよう。前者の場合には計算処理に使われるデータは

	<foo>.....</foo>
のようにタグで囲い、"....." の中には他のタグを入れるべきではない*。しかし後者の場合にそのような制約を課していたら文書を作れない。

注*: この事は、子供を持つノードは、テキストデータ部を持つべきではないことを意味している。XBRL に関してはこの規則が守られている。また mktree()mkidf()mkxml() もこの規則が守られていることを前提にしている。

XML の仕様書は膨大であり、読む気にもなれない。膨大になって行く原因は、スーパーセットを目指しているからだ。しかしスーパーセット主義は失敗する事を歴史が教えている。

ムーアの法則が示しているように、コンピュータの処理能力は指数関数的に発展している。プログラムの複雑さはどうか? Python や Lua を使っていると、随分楽になったなと思う。新しい言語の出現がプログラムの複雑さを時々大きく緩和してくれるのだ。しかし規格書の複雑さはどうか? XML の仕様書を見ると、プログラマが読まなくてはならない文書が指数関数的に増大し複雑になっていると感じる。やがては人間の能力の限界を越えるであろう。何かがまずいのである。

HTTP のように TCP レベルのプログラムを書くプログラマは、その下位のレベルのプロトコル(IP や イーサネットレベル)を詳しく知る必要は無い。HTML レベルの文書を作る場合にも、同様に HTTP レベルの詳しい知識は要らない。そのような階層化された構造を持っているからこそ、発展可能なのである。XML に関しては無理なのか?