プログラム言語Lua 配布プログラムについての補足

Luaを良く理解するための補足的な解説です。

配布されているLua言語に含まれているサンプルプログラムは、 なかなか奥の深いものから歴史的なものもあり、初心者にはすこし 難しいと思われる面もあります。徐々に追加更新していきますので、コメントなどありましたら、メール uenoyt(a)ni.aist.go.jp などでご連絡よろしくお願いします。

サンプルコードについての解説

bisect.lua 2分法で解く非線形方程式(bisection)

非線形関数としてf(x)を定義して、solve()関数によって、 指定した区間[a,b]の中でf(x)=0となるxを求めます。 解を求めるアルゴリズムの本体はbisect()で、区間a,bの範囲を狭く変更しながら 自分自身を再帰的に呼び出します。そして、a,bの差が微小な値として 与えたdeltaより小さい区間になるまで繰り返して 得たaの値が解となります。

cf.lua セ氏温度から華氏温度への変換

アメリカでは、セ氏温度でなくて華氏温度が使われます。カーニハン・リッチーのC言語の教科書に 出てくる古典なのですが、日本ではセ氏温度しか使われていないのですぐに理解できないのが残念です。 日本で言うなら、お酒が一升ビンで約1.8リットル入っているという話です。 どうしてリットルを使わないのか、とか考え出したら大変ですが、きっと似たような事情だと思います。

echo.lua コマンド行を出力する

標準のLuaインタプリタでは、指定したコマンド行は、argという大域変数に入れられます。 これを出力する例です。"-l" などのオプションは取り除かれます。

env.lua OSの環境変数を自動的に大域変数にする方法

メタメソッドを使うと、大域変数を使おうとしたときに特別な関数を呼ぶことができます。 その中で、オペレーティングシステムで設定する環境変数を取得する関数を呼ぶよ うにすることで、luaから自動的に環境変数を使えるようになります。 ここでは、indexメタメソッドを指定した例で、既に定義されている大域変数については そのままで、定義されていないものは、その名前で環境変数を参照にいきます。

factorial.lua 再帰を使わない階乗の計算

Luaでは再帰的呼び出しを効率よくする手法として末端呼び出し(tail call)があります。この例は、末端呼び出しになっていない場合でも、 末端呼び出しで実行できるというもので、クロージャの強力な機能の例です。 難しい話をすれば、LISPとかラムダ計算の不動点演算子 fixed point operator です。そうした情報科学的な話はwikipediaなどを参照していただくとして、どういう考えなのかを説明します。さて、階乗の計算は再帰的呼び出しを使って次のように定義できます。
function factk(n)
	if(n==1) then 
		return 1
	else
		f=n*factk(n-1)
		return f
	end
end
末端呼び出しというのは 関数が自分自身を呼び出す時に、
return factk(n-1)
のように、その結果をそのままreturnする形のことです。 この階乗の場合は、nを掛け算しなければならないので、末端呼び出しになりません。 通常は、関数呼び出しの時は、今使っている変数の内容をとっておくために、 何キロバイトかのメモリをスタックにとって自動的に保存します。 しかし、すぐにreturnする場合はその必要がないので、実行を ジャンプするだけにすることができます。これが末端呼び出しで、 1000回くらい再帰的呼び出しされた際に、スタックにメモリが大量に 必要になって実行不可能になるという、いわゆるスタックオーバーフローの エラーを防ぐことができます。Programming in Lua の6.3節にも解説があります。 この階乗の場合は、末端呼び出しになっていないのに、これを 末端呼び出しにしてしまうというテクニックです。その一方で、 ソースコードは超絶に難解です。

fib.lua フィボナッチ数列を発生する


このサンプルコードでは、名前なし関数と、関数の動的な定義とファーストクラス変数としての関数を扱います。 キャッシュを使った関数の高速化は、メモ化としても知られています。一度計算した結果を保存しておき、2回目からはその値を使い、計算を省略します。メモ化(meomoise)とも呼ばれ、programming in Lua に解説されています。 このキャッシュ関数では、一変数の関数を扱うことにします。関数を与えると、キャッシュを使ってより高速な新しい関数を返すものに変更します。fibという関数があったときに、次の様に使います。
fib_new=cache(fib)
この関数は中にテーブルをひとつ持っていて、過去に行った計算をとっておきます。それはローカル変数cですが、新しい関数がアップバリューとして使っているため、外から見えない専用テーブルとなります。同様にして、与えられた関数は、新しい関数が生きている間はずっと参照しつづけ、グローバル変数が抹消されても、元の関数の実体をつかんでいます。
function cache(f)
	local c={}
	return function (x)
		local y=c[x]
		if not y then
			y=f(x)
			c[x]=y
		end
		return y
	end
end
キャッシュの効果を確かめるために、testtime()関数を作って次のようにして使います。
testtime("メッセージ"、関数、関数へのパラメータ)

fibfor.lua コルーチンによるジェネレータ関数

ジェネレータ関数は、別スレッドとして並列に動作し、 呼ばれた元のプログラムが処理をしている間は中断しています。 より簡単な例は、ステップずつ変化させるジェネレータです。
  -- example of for with generator functions

  --- first, it is good to know how the generator function
  --- works as a coroutine to help your loop 

function valstep(val,vend,step) 
	  --- this is a function runs behind your loop
	  --- just to calculate next loop variable
	local f=function()
		while(val<vend) do
			coroutine.yield(val)  --- give a return value ss
			val=val+step
		end
	end
	return coroutine.wrap(f)
		--- a function to resume the specified function
		--- so the 'for' statement will call the function
		--- until yield() that gives a return value at that time
end

for i in valstep(0,10,2) do 
	print(i)	--- output "0 2 4 6 8 10" 
end
ジェネレータ関数とは、ループ処理の次のステップを関数で記述する関数 です。yield関数で指定した値が、次のループ処理変数の値になります。 ループで1から順番にフィボナッチ数をnまで計算して、 順番にyield()関数を呼ぶという構成にします。  ここでは、f(n)とf(n+1)は変数a,bです。一番最初に、n=1の時の値を返します。 f(1)とf(2)は初期値として1が入れてあり、f(1)=1が得られます。 yield()関数は、指定した値を関数のリターン値として 返したあと、終了せずに待ち状態となります。resume()関数に よって再開されますが、ここではwrap()関数によって その中でresume()関数を呼びます。再開、次はyield()の次 からです。 n+1の場合はすでにbに計算されているのでこれを aにいれ、その次は、f(n+2)=f(n)+f(n+1) これはa+bですから、bに入れて用意しておきます。
  --- so let us apply this loop to calcurate
  --- the fibonacci array. the generator function will
  --- create a coroutine that calcurate values until 'n'
 
function generatefib (n)
  local f=function ()
    local a,b = 1, 1
    while a <= n do
      coroutine.yield(a) --- give a return value a 
      a, b = b, a+b
    end
  end
  return coroutine.wrap(f)
end

for i in generatefib(100) do print(i) end

globals.lua 大域変数の利用状況調査

ほとんどのプログラム言語では、大域変数をどのくらい使ったのかは自分でしっかり覚えていなければなりません。これは一覧表をつくるツールで、luacコンパイラの機能として、-lでコンパイルされた仮想マシン語を出力します。大域変数を参照するマシン語命令がGETGLOBALで、その行のセミコロン以下のコメントにプログラム中の変数名が書かれているので、それを出力するという文字列処理です。SETGLOBALも同時に検索しています。

trace-globals.lua 大域変数への代入のトレース

Luaでは大域変数を実行時にチェックできる機能があり、大きな特徴の一つです。 具体的な方法は、バージョンが上がるにつれて機能アップされて少し難しくなっていますが、基本的には、大域変数が一つのテーブルで表されること、代入があった時に 指定した関数が呼び出せることです。これはLuaの代表的な機能の一つで、バージョン3まではフォールバックと呼ばれていましたが、その後タグメソッド、メタメソッドと拡張してきました。大域変数を表すテーブルのnewindexイベントに対して呼ばれるメタメソッドとして扱います。

hello.lua どの言語にもある最初のプログラム

変数_VERSIONには、Luaインタプリタのバージョン番号が入ることになっているので、インタプリタのバージョンを確かめるのにも使われます。ところでこうした出力を確かめるのはプログラムの動作確認の定番なのですが、どうして Hello Wolrd なのでしょうか。それは、プログラム言語を使うことによって、コンピュータの動作を一から決めて動かすことができるわけで、その祝福すべき第一歩となる言葉だからです。 プログラム言語が発明された頃の話ですが、ガレージで作った計算機が動作した時というのは、プログラムに書いたメッセージが初めてこの世に通じた瞬間でした。それもタイプライターのようなプリンタで印刷したのですが、まるで生まれてきたヒヨコに「ようこそこの世界に」と返したくなるような素直な出力だったのです。一方、今のコンピュータは、電源を入れた時からきれいな画面が出ますし、コンピュータの方が世界のことを良く知っていそうです。でも、プログラム言語を使うと、動作を一から教え込むヒヨコのようになります。

printf.lua C言語のようなprintf関数の例

関数に与えるデータはしばしば引数と呼ばれますが、 Luaでは関数のパラメータと考えてかまいません。 ここでは、可変個のパラメータを扱う例です。ピリオドを3つ使って "..." という書き方で、可変個のパラメータを表します。ちなみに、C言語ではprintf関数を使う際には、変数の型をあわせる必要があり、注意しなければ思わぬエラーが発生する厄介ものでしたが、Luaではそのような問題がありません。変数に型がなく、自動変換されるためです。そして、可変個のパラメータを扱う時のエラーも起こらないよう、よく研究された言語になっています。C言語などでは、こうした関数の引数について特別な注意が必要ですが、その心配がないのもLuaの特徴です。

readonly.lua 大域変数を書き込み禁止にする方法

関数や部分的なモジュールを作成した時には、大域変数を変更 しないのが一般的です。また、大域変数は、定義してから使うように するのは、良いプログラミングの習慣です。メタメソッドを使って 代入を禁止にすると、そうしたルールを厳しく設定できるため、デバッグに役立ちます。 メタメソッドには、error()関数を呼んで処理を停止していますが、 完成したアプリケーションで用いる際は メッセージだけ出して継続しても良いでしょう。こうした処理が柔軟に できることはLuaの大きな特徴です。 error()関数では、2番目のパラメータに2を指定し、エラーハンドラ関数 が呼ばれた場所をトレースする指定をします。

sieve.lua エラトステネスのふるいで素数を求める。コルーチンで作成

1からNまでの間の素数を出するプログラムです。このアルゴリズムは wikipediaに図解もされているように、素直に作成するならば、 まず最初に2から順に並べた数値表をつくるのですが、 このプログラムでは、そうではなくて 複数のコルーチンを使っています。 このコルーチンは、それぞれ一つの素数を担当していて、与えられた数自分の素数の倍数になっているか どうかを判断する役目をもっています。 2、3,5と順番に初めてNまでの素数について、チェックすることになります。 プログラムでその処理はfilter()関数がやていて、剰余が0かどうかです。 さて、最初のgen()関数で、2からNまでのループ処理を 書いていますが、この関数は、xという変数に入り、 あとで実行されるものです。具体的には、 filter()関数の中のg()関数が実行された時です。 読みにくいプログラムと思われるかもしれませんが、 ループ処理がxという変数になってあちこちで 実行されるという形になっています。従来のプログラム言語では、 ループ処理はプログラムの骨組みそのものであって、 変数で渡したりするものではなかったのですが、 そういう型にはまらない新しい方法といえます。

sort.lua ソート関数の2つの構成例

Luaでは、標準ライブラリにソート関数がありますが、アルゴリズムを開発 する際にはこうしたソースコードが役に立つでしょう。

table.lua 同じ内容のデータをまとめる

2つのデータが並んだファイルを、項目と値の組として、 同じ項目の値を並べる処理です。sting.find()関数で、スペース文字で 区切られた2つのデータを取り出します。変数Aに一つ前の項目名をとっておいて 同じものかどうか比べています。

life.lua Conwayのライフゲーム

碁盤のようなマス目の上でで、単純な原理で生存競争のシミュレーションをする プログラムです。アルゴリズムはwikipediaでも解説されているように、 生命というにはあまりにもシンプルなモデルです。 ちなみに、windowsのコマンドプロンプトで実行すると、画面表示が流れてしまうのですが、これは画面をクリヤするエスケープシーケンスが効かないためです。 とりあえず見てみるには、103行目にある画面制御のwrite()関数のところに、 os.execute("cls") を入れると、シミュレーションの様子を みることができるようになります。効率は良くないのですが。

luac.lua 基本機能だけのluaコンパイラ

標準のluaコンパイラとしてluacがありますが、コンパイラとしての基本機能はLuaプログラムからも利用できるようになっています。その使い方として、基本機能だけのLuaコンパイラになるようにしたものです。主な部分はstring.dump()関数で、Luaのプログラムを翻訳して、実行可能な形式に変換します。変換した結果は文字列データとなりますが、表示できるものでなくて、これをファイルに入れれば実行可能なLuaプログラムとして動作します。つまりこれがLuaインタプリタとなるわけです。ファイルに入れなくても、loadstring()関数を使うとそれを実行できる関数が作られるので、その関数を呼べば実行することができます。コンパイルしてあってもなくても同じです。以前のバージョン5.0まではすぐに実行するdostring()という関数を使っていましたが、ギリギリまで機能を節約するのがLuaのポリシーです。この機能も標準はloadstring()に変更になりましたが、dostring()を定義するのは簡単です。

trace-calls.lua 関数呼び出しのトレース

luaプログラムを実行する前にこのコードを実行すれば、プログラムでのすべての関数呼び出しをリスト出力する、デバッガのような機能が実現できます。プログラムの先頭でdofile()関数またはrequire()関数で呼び出してもよいのですが、luaインタプリタで、" -l "オプションをつけて指定すれば先に実行してくれます。unixのコンパイラで慣習的に使われるライブラり指定と同様です。
	lua -ltrace-calls  bisect.lua
	
Luaのデバッグ機能関数であるdebug.sethook()は、関数呼び出しがあるごとに 指定した関数を呼び出します。そして、 getinfo()は、現在実行している関数に ついての情報をテーブルに入れて返します。パラメータに1を指定すると getinfo()関数を よんだプログラム、2ではその一つ前の呼び出し関数、とさかのぼりますが、 フック関数の中では2がフック関数自体に対応するので、 3を指定してフック関数が呼ばれたところの関数を指定します。 このフック関数は、Luaの初期のバージョンから提供されている Luaの特徴の一つです。処理系の内部に関わるような、様々な事ができる 柔軟さが魅力の一つです。

xd.lua 16進数でファイル内容を表示

ファイルの内容が文字列でない場合に、16進数で表示することをヘキサダンプと言います。使い方は、lua xd.lua <file のようにします。windowsのコマンドプロンプトではdebugコマンドでも同様の出力ができます。Luaではこうしたバイナリファイルの扱いもできるのですが、浮動小数点の数値データは標準では対応していないので、別途C言語でモジュールを作成する必要があります。



更新2010年2月, 上野豊 産総研