1. ホーム
  2. スクリプト・コラム
  3. ルア

Luaで学ぶ関数とオブジェクト指向の基本

2022-01-06 02:39:14

関数
1. 基本的なこと
関数の呼び出しは、引数がない場合でもすべて括弧を書く必要がありますが、1つだけ特別な例外があります。引数が1つだけで、引数がリテラル文字列またはテーブルコンストラクタである関数は、括弧があってもなくても構いません。たとえば dofile 'a.lua', f{x=10, y=20}のようになります。

Luaでは、コロン演算子を使ったオブジェクト指向の呼び出しに特別な構文が用意されており、例えばo.foo(o, x)はo:foo(x)と等価です。Javascriptと同様に、関数を呼び出す際に提供される実パラメータの数は、形式パラメータの数と異なる場合があります。実パラメータが多ければ捨てられ、少なければ余分な形式パラメータはnilに初期化される。

1.1 複数の戻り値

Luaでは、関数が複数の結果を返すことができます。例えば、s, e = string.find("hello Lua world", "Lua") のように、関数はmax, index, receiveのような結果を返します。関数呼び出しが一連の式の最後の要素でない場合、生成される値は1つだけです。

function foo() return "a", "b" end
x, y = foo(), 20 -- x="a", y=20 (the second return value of foo is discarded)
print(foo() ... "x") -- output ax, this is because Lua adjusts the number of return values to 1 when the function appears in an expression

また、戻り値は関数呼び出しが最後の要素のときだけ調整されるのではなく、それ以外の位置では1に調整されます。例えば、t = {foo2()} then t = {"a", "b"}, t = {foo2(), 4} then t = {"a", 4}のようになります。

特殊関数unpackは、引数として配列を受け取り、添え字1から始まる配列のすべての要素を返す。例えば、a, b = unpack({10, 20, 30})、その後30は捨てられる。unpackの重要な使用法は、「ジェネリックコール」メカニズムである。

1.2 可変長引数

関数の引数リストにある 3 つの点 (...) は、その関数が異なる数の実パラメータを受け取ることができることを示しています。Lua 5.0では、"... "式は提供されていませんので、可変長引数を反復処理するには、関数内で暗黙的にローカル変数argにアクセスします。固定引数がある場合は、可変長引数の前に配置する必要があります。

2. 高度なトピック
2.1 クロージャ関数

基本的にはJavascriptのクロージャと同じものなので、ここでは触れないことにします。技術的には、Luaには「関数」ではなく「クロージャ」しかありません。関数自体が特殊なクロージャだからです。クロージャは、高階関数の引数、GUIツールキットのコールバック作成、関数の再定義と新しい実装での古い実装の呼び出し、「サンドボックス」な安全実行環境の作成など、様々な用途で使用されています。

2.2 非グローバル関数

Luaのライブラリの多くは、関数をテーブルに格納する仕組み(例:io.read、math.sin)を採用しており、例えば、以下の3つの方法で、テーブルのメンバ関数を定義することができます。

MathLib = {
  plus = function(x, y) return x + y end
}
MathLib.minus = function(x, y) return x - y end
function MathLib.multiply(x, y) return x * y end

ローカル関数の定義

local f = function(<arguments>) <function body> end
local function f(<argument>) <function body> end -- Syntactic sugar provided by Lua

**注意:再帰的な関数を定義する場合、上記の最初の定義は使用できず(関数本体がfを呼び出すときにはまだfが定義されていないため)、2番目の「シンタックスシュガー」を使用しますが、これは問題ありません。または「フォワード宣言」を使用して、最初にローカルf、次にf = function ...とします。このように定義します。

2.3 正しいテールコール

関数呼び出しは、他の関数の最後の動作である場合、「テールコール」とみなされます。例えば、function f(x) return g(x) end. は何回も呼び出すことができます。テールコール」のように見えるコードでも、実はこのルールに違反しているものがあります。

function f(x) g(x) end -- After calling g, f does not return immediately, and the temporary result returned by g needs to be discarded
function f(x) return g(x) + 1 -- one more addition to do
function f(x) return x or g(x) -- must be adjusted to a return value

そのため、return形式のコールのみ ( ) はテールコールとみなされます。

オブジェクト指向プログラミング
Luaにおけるテーブルは、オブジェクトと同じように状態を持つことができ、また、オブジェクトと同じように値から独立したアイデンティティ(自己)を持ち、さらに、オブジェクトと同じように作成者から独立したライフサイクルを持つことから、オブジェクトの一種と言えます。しかし、Luaにはクラスの概念がなく、メタテーブルでプロトタイプを実装し、プロトタイプでクラスや継承といったオブジェクト指向の機能を模擬することしかできない。今回は、オブジェクト指向プログラミングに関するLuaの紹介をします。

1 セルフとコロンの構文

self引数の使用はすべてのオブジェクト指向言語の核心であり、Luaはコロン構文を使用するだけでそれを隠すことができ、例えば次の2つのコードは等価である。

Account = {balance=0}
funtion Account.withdraw(self, v)
  self.balance = self.balance - v
end
a1 = Account; Account = nil
a1.withdraw(a1, 100.0) -- note that this is runnable

function Account:withdraw(v)
  self.balance = self.balance - v
end
a2 = Account
a2:withdraw(100.0) -- omits the a2 argument passed in


2 クラスの書き方

プロトタイプベースの言語では、オブジェクトは型付けされませんが、すべてのオブジェクトはプロトタイプを持っています。プロトタイプとは、未知の操作に遭遇したときに、まず他のオブジェクトを調べる通常のオブジェクトのことである。このような言語でクラスを表現するには、他のオブジェクトのプロトタイプとなる専用のプロトタイプを作成するだけです。Luaではプロトタイプの実装は簡単で、メタテーブルの__indexを使用して継承を実装するだけです。

(テーブルに存在しないフィールドキーにアクセスした場合、一般にnilという結果が得られます。実際、このアクセスはインタープリタに__indexというメタメソッドを探すよう促し、もしそのようなメタメソッドがなければ、アクセス結果は前と同じくnilとなり、それ以外はこのメタメソッドにより結果が提供されます。(メタメソッドは関数であるだけでなくテーブルであることも可能で、 テーブルであればそのテーブルのキーに対応するコンテンツを直接返します)。

aとbの2つのオブジェクトがある場合、bをaのプロトタイプにするには、setmetatable(a, {__index=b}) とするだけです。

function Account:new(o)
  o = o or {} -- if the user does not provide a table, create a
  setmetatable(o, self)
  self.__index = self
  return o
end

a = Account:new{balance = 0} が呼ばれると、a は Account (関数内の self) をメタ・テーブルとして受け取ります。a:withlow(100.0)が呼ばれると、Luaはテーブルaからエントリwithlowを見つけられず、さらにメタテーブルの__indexエントリ、すなわちgetmetatable(a)を検索します。__index.withdraw(a, 100.0)となります。新しいメソッドでは self.__index = self となるので、上記の式は再び Account.withdraw(a, 100.0) となり、self パラメータとして a が渡され、再び Account クラスの withdraw 関数が呼び出されるのです。このオブジェクトの作成方法は、メソッドだけでなく、新しいオブジェクトに含まれない他のすべてのフィールドに対しても有効です。

3 継承

さて、AccountクラスからSpecialAccountのサブクラスを派生させる(顧客がオーバードローできるようにする)には、単純に。

SpecialAccount = Account:new()
s = SpecialAccount:new{limit=1000.00}

SpecialAccountはAccountからnewを継承し、SpecialAccount:newが実行されるとそのselfパラメータはSpecialAccountとなり、sのメタテーブルはSpecialAccountとなる。sに存在しないフィールドを呼び出す場合はルックアップされますが、新たにリネームしたメソッドを書いて親クラスのメソッドをオーバーライドすることも可能です。

4 多重継承

上記の紹介で、__indexメタメソッドにテーブルを代入したのは、単一継承を実装するためです。多重継承を行うには、__indexフィールドを複数の基底クラスのメソッドフィールドを検索する関数にします。この検索が複雑になるため、多重継承は単一継承ほどにはうまくいきません。パフォーマンスを向上させるもうひとつの簡単な方法は、継承したメソッドをサブクラスにコピーすることですが、この方法の欠点は、変更が継承システムの下に伝搬しないため、システムが稼働するとメソッド定義を修正することが難しくなることです。

5 プライバシ-性

Luaはオブジェクトを設計する際にプライバシーの仕組み(private)を提供しませんが、その様々なメタメカニズムにより、プログラマはオブジェクトのアクセス制御をシミュレートすることができます。この実装は一般的に使われているものではないので、基本的な理解だけにしておきます。オブジェクトは、オブジェクトの状態を保持するテーブルと、オブジェクトの操作(つまりインターフェース)を保持するテーブルの2つで表現されます。

{{コード

プライバシー機構は、プライバシーが必要なフィールド(残高など)をクロージャによってselfテーブル内に保持し、 withdrawインターフェースのみを公開することで実現されている。