読者です 読者をやめる 読者になる 読者になる

Minecraftとタートルと僕

PCゲームMinecraftのMOD「ComputerCraft」の情報を集めたニッチなブログです。

【補足】Luaの型は色々あるけれど関数型って特殊なんです

はじめに

今回はLuaというプログラム言語について詳しくお話しましょう。
例によって長いので、結論だけ知りたい人、特にCのポインタを理解できている人なら、最後のまとめだけ見たらOKですよ。

Luaが扱う値のタイプ

Luaでは様々な値を扱いますが、その値にはいくつかのタイプ(ここでは型と呼びます)があります。
正確にはLuaリファレンスマニュアルを見てもらうとして、CCプログラマがよく使う値としては、

  • 文字列: "ABCDE" など。改行コード"\n"などの制御コードキャラクタもこれに含む
  • 数値: 1.33333 など。整数、実数、正の数、負の数など区別せずにこの型で扱う
  • nil: 変数初期化時に、初期値を決めないとき自動的にその値はnilとなる。
  • ブーリアン: true または false
  • テーブル: {name=3,"A"}など。連想配列。テーブルコンストラクタなどを使って定義できる。
  • 関数: 全て無名関数。詳しくは後述。

などのタイプがあります*1

これらの値はどれでも変数の中に代入し、またその変数を通して値を参照することができます。
C言語などでは変数使用時に変数の型を宣言しなくてはなりませんが(int x; など)、Luaの変数に型はありません。
値を入れる箱自体に制限はないので、上に挙げたどのタイプの値でも変数に入れてかまいません。

値のタイプを調べる方法

それではここで値の型を調べてみましょう。
「lua」コマンドを実行してLuaインタプリンタを実行します。そして以下のようにコマンドを実行すると、値の型を調べることができます。

lua> type("ABCDEFG12345")
string

lua> type(12345)
number

lua> type({x=3,y=4})
table

lua> type(nil)
nil

lua> type(true)
boolean

さて、では関数はどうやって調べるのでしょう。こんな感じ?

lua> type(os.sleep(0))
英語でわけわからんエラーw

だめでしたw 
実はこれだと「os.sleep(0)」という関数を実行した結果を調べることになってしまうのです。os.sleep()関数は返し値を持たない関数なので、実質的に「type()」と引数無しで実行したのと同じ結果になってしまうのです。ですからここでのエラーは「type()関数は引数が1つ必要なんですよ(なのに1つも無いよ)」という意味です。

Luaにおける関数は特殊な型なのです

これまで、関数定義は次のようにやってきました。

function hevo(x)
  print(x)
end

でも実は、この書き方は内部的に次のように変換されています。

hevo = function(x)
  print(x)
end

え?何が違うのかって? 

  • 「hevo(x)」という名前の関数を定義した ←厳密に言うと間違い
  • 「引数xをとりprint(x)を実行する」という無名関数を作って、その無名関数を変数「hevo」に代入した。←厳密な意味ではこちらが正解

無名関数? なにそれ? もう少し詳しく説明しましょう。

--上記の関数定義の一行表現
lua> hevo = function(x) print(x) end
 
lua> type(hevo)
function
 
lua> hevo
function: 3cb6273e

type()によって、変数「hevo」の中身は関数であることがわかりました。
そして変数「hevo」をそのまま実行すると、↑の最終行のような表示が行われます。
これは、変数「hevo」に入っているものが、メモリー上の3cb6273eという住所に保存されている何らかの関数であることを意味しています。
名前がついていない関数なので、その保存場所の住所で識別するしかないのですよね。
例えるならば、「3丁目の角に住んでいる名前がわからない一人暮らしの元気なお婆さん」のことを、便宜上「3丁目のお婆さん」と呼ぶようなものです。

では、この無名関数を実行するためにはどうするか。それは引数を渡してあげればいいのです。
もし関数定義時の引数が1個ならば1個の引数を、0個ならば0個の引数を渡した上で実行します。たとえば次のように。

lua> hevo = function(x) print(x) end
 
lua> hevo("abc")
abc
 
lua> hevo()
 
  • ↑の最終行は空白改行です。引数を1個必要とする関数に0個の引数を与える(引数無しで括弧のみを記述する)ことでprint()が実行されました。

なお余談ですが、前回のloadstring()は、文字列から無名関数を作る関数です。つまり、以下の2つは似たような働きをする無名関数をそれぞれ作っています。
x()、y()と実行すると、両方とも3と表示します。

x = function()  print(3)  end

y = loadstring("print(3)")

つまり全ての関数は無名関数なんだよ!

つまりLuaにおいて、全ての関数は無名変数なのです!
関数に名前をつけているようにみせかけて、実はあれは、無名関数を入れた器(変数)の名前でしかないのです。

さあこれで、次のような記述は、これからこう解釈できるようになりますね?(以下の住所は適当なので、あしからず)

  • sleep(10)
    • 関数名「sleep」を引数10を渡して実行した。
    • 変数「sleep」に「3丁目にある関数」と書かれていたよ。そこでその関数を探してきて、「10」という引数与えて関数実行したよ。
  • exit()
    • 変数「exit」に「4丁目にある関数」と書かれていたよ。そこでその関数を探してきて、引数0個を渡して実行したよ。

では「turtle.select(3)」はどうでしょう。この「.」ドット区切りついては少しだけ注意が必要です。

  • turtle
    • CCでは「turtle」はTableとして作成済みです。つまりは連想配列です。この中には複数の(キー、値)のペアが含まれており、ドット記法で値を参照できます。
  • つまり、 turtle.select  は、 turtle["select"] に等しいのです。
    • この解釈は次のとおり: テーブル「turtle」の中の値についてキー「select」でアクセスしたよ。そしたら、「5丁目にある関数」って書かれてた。
  • そしてさらに、turtle.select(3)
    • ・・・省略・・・、「5丁目の角の関数」って書かれてたから、そこでその関数を探してきて「3」という引数を与えて実行したよ。

無名関数の取り扱い

これまでは関数名「○○」と呼んできましたが、内部的には、「○○」は変数名のことでしかありません。
そしてこの変数の中には、「5丁目の関数」といった住所情報しか書いてない。

住所情報なんていくらでもコピーできますよね。

lua> org_func = turtle.select

lua> turtle.select
function: 4bc2ad30

lua> org_func
function: 4bc2ad30

これで、「turtle.select」に書かれていた住所を変数「org_func」にも書くことができました。
この2つの変数、どちらを使っても同一の関数を実行することができるようになりました(「turtle.select(1)」と「org_func(1)」は、同一の関数を実行することになります)。

ここで重要なことは、住所情報を書いた箱(変数)が増えただけであって、無名関数そのものがコピーされて増えたわけではないことです。

無名関数に対するなんらかの操作は、関数自身ではなく、その関数の住所情報を使った間接的な操作になります*2

それ以外の値、たとえば文字列や数値などに対する操作が、値そのものを使った直接操作であることと対称的です。

住所で扱うので、関数本体には触らず柔軟な操作が可能

そして住所なんていくらでも偽造可能ですよね。
「3丁目のお婆さん」を改造したら人道に触れますが、
「ファンキーなお婆さんロボット」を新しく作成して、最初の「turtle.select」にその設置場所の住所を書いてあげれば、何の問題もなく呼び出すお婆さんを入れ替えることができます。
「turtle.select(スロット番号)」で関数を呼び出すたびに、ファンキーなお婆さんに会えますよ!!

lua> org_func = turtle.select
lua> turtle.select = function(n) print("Wowow!") org_func(n) print("YeahYeah!") end 
lua> turtle.select(10)
Wowow!
(選択スロットを10に変更)
YeahYeah!

つまり、無名関数が理解できれば、既存の関数の上書き機能拡張が簡単にできるのです。
これはとても便利です。使い方によっては少しだけ危険ですけどw
でも、「org_func」に元の関数の住所情報が書いてあるので元に戻すのは簡単だし、もっと手軽にCCコンピュータをリブート(CTRL+R)するだけで元に戻すことができるので大丈夫。問題ありません。
既存の関数の機能に不満があったら、どんどん上書き機能拡張に挑戦してみましょう*3

まとめ

  • Luaにおける関数は全て無名関数。
  • よってLuaにおける関数定義とは、無名関数を作ってそれを一時的に変数に代入すること。
  • だから sleep(10) を厳密に解釈すると
    • 関数名「sleep」を引数10を渡して実行した。 ←厳密に言うと間違い
    • 変数「sleep」に「○丁目△番地にある関数」と書かれていた。そこでその関数を探してきて「10」という引数与えて関数実行した。
    • つまり「sleep」は無名関数を指す変数、「()」は引数を渡した上で関数を実行するための式
  • 既存関数の機能に不満があったらどんどん書き換えようぜ!!
    • 元の関数のバックアップをとっておけば、元に戻したくなってもモーマンタイ
    • どんなに失敗しても、CTRL+RでリブートすればだいたいOK

*1:正確にはユーザーデータなど他にもあるけれど、ここでは省略。

*2:LuaにおいてはTableも住所情報を使った間接的な操作になります

*3:個人的には、Turtle APIの中に現在の選択スロットを知る関数がないのが不満です。turtle.select(10)のように選択スロット決めうちだけじゃなくて、turtle.getSelectedSlot() とか turtle.selectNext() のような関数欲しい。