,
a = 42 --> ...">

Luaの関数引数の型制御

挑戦する



Luaは動的型付け言語です。



これは、言語の型が変数ではなく、その値に関連付けられていることを意味します。



a = "the meaning of life" --> , <br/>

a = 42 -->






これは便利です。



ただし、変数の型を厳密に制御したい場合がよくあります。 最も一般的なケースは、関数の引数をチェックすることです。



素朴な例を考えてみましょう:



function repeater ( n, message ) <br/>

for i = 1 , n do <br/>

print ( message ) <br/>

end <br/>

end <br/>

<br/>

repeater ( 3 , "foo" ) --> foo <br/>

--> foo <br/>

--> foo






repeat関数の引数を混同すると、ランタイムエラーが発生します。



 >リピーター( "foo"、3)
 stdin:2: 'for'制限は数値でなければなりません
スタックトレースバック:
	 stdin:2:関数 'repeater'内
	 stdin:1:メインチャンク内
	 [C] :?


「これは何のためですか?」-私たちの関数のユーザーは、このエラーメッセージを見ると言います。



この機能は突然ブラックボックスではなくなりました。 内臓がユーザーに見えるようになりました。



誤って2番目の引数を渡すのを忘れると、さらに悪化します。



 >リピーター(3)
なし
なし
なし


エラーは発生しませんでしたが、動作は潜在的に正しくありません。



これは、関数内のLuaでは、渡されなかった引数がnilに変わるためです。



オブジェクトメソッドを呼び出すときに別の典型的なエラーが発生します。



foo = { } <br/>

function foo:setn ( n ) <br/>

self.n_ = n<br/>

end <br/>

function foo:repeat_message ( message ) <br/>

for i = 1 , self.n_ do <br/>

print ( message ) <br/>

end <br/>

end <br/>

<br/>

foo:setn ( 3 ) <br/>

foo:repeat_message ( "bar" ) --> bar <br/>

--> bar <br/>

--> bar






コロンは、最初の引数をオブジェクト自体に暗黙的に渡す構文糖衣です。 例からすべての砂糖を削除すると、次のようになります。



foo = { } <br/>

foo.setn = function ( self, n ) <br/>

self.n_ = n<br/>

end <br/>

foo.repeat_message = function ( self, message ) <br/>

for i = 1 , self.n_ do <br/>

print ( message ) <br/>

end <br/>

end <br/>

<br/>

foo.setn ( foo, 3 ) <br/>

foo.repeat_message ( foo, "bar" ) --> bar <br/>

--> bar <br/>

--> bar






メソッドを呼び出すときに、コロンではなくピリオドを記述すると、selfは渡されません。



 > foo.setn(3)
 stdin:2:ローカルの 'self'(数値)のインデックス付けを試みます
スタックトレースバック:
	 stdin:2:関数 'setn'で
	 stdin:1:メインチャンク内
	 [C] :?

 > foo.repeat_message( "bar")
 stdin:2: 'for'制限は数値でなければなりません
スタックトレースバック:
	 stdin:2:関数 'repeat_message'内
	 stdin:1:メインチャンク内
	 [C] :?


少し気を散らしましょう



setnの場合、エラーメッセージが十分に明確な場合、repeat_messageのエラーは一見すると神秘的に見えます。



どうしたの? コンソールをより詳しく見てみましょう。



最初のケースでは、インデックス「n_」の値を数値に書き込みます。



 >(3).n_ = nil


完全に合法的に回答されたもの:



 stdin:1:数値のインデックス付けを試みます
スタックトレースバック:
	 stdin:1:メインチャンク内
	 [C] :?


2番目のケースでは、同じインデックス「n_」の文字列から値を読み取ろうとしました。



 >リターン(「バー」)。
なし


すべてがシンプルです。 メタテーブルはLuaの文字列型に添付され、インデックス作成操作を文字列テーブルにリダイレクトします。



 > getmetatable( "a").__ index == stringを返します
本当


これにより、文字列に省略表現を使用できます。 次の3つのオプションは同等です。



a = "A" <br/>

print ( string.rep ( a, 3 ) ) --> AAA <br/>

print ( a:rep ( 3 ) ) --> AAA <br/>

print ( ( "A" ) :rep ( 3 ) ) --> AAA






したがって、文字列からインデックスを読み取る操作は、 文字列テーブルでアクセスされます



記録が無効になっているのは良いことです:



 > getmetatable( "a")を返す.__ newindex          
なし
 >( "a")._ n = 3
 stdin:1:文字列値のインデックス付けを試みます
スタックトレースバック:
	 stdin:1:メインチャンク内
	 [C] :?


文字列テーブルにはキー「n_」がありません。そのため、上限ではなくnilをスリップしたことを誓います。



 > i = 1の場合、文字列["n_"] do
 >>印刷(「バー」)
 >>終わり
 stdin:1: 'for'制限は数値でなければなりません
スタックトレースバック:
	 stdin:1:メインチャンク内
	 [C] :?


しかし、気が散りました。



解決策



そのため、関数の引数の型を制御したいと思います。



簡単です。チェックしてみましょう。



function repeater ( n, message ) <br/>

assert ( type ( n ) == "number" ) <br/>

assert ( type ( message ) == "string" ) <br/>

for i = 1 , n do <br/>

print ( message ) <br/>

end <br/>

end <br/>







何が起こったのか見てみましょう:



 >リピーター(3、 "foo")
 foo
 foo
 foo

 >リピーター( "foo"、3)
 stdin:2:アサーションに失敗しました!
スタックトレースバック:
	 [C]:関数 'assert'内
	 stdin:2:関数 'repeater'内
	 stdin:1:メインチャンク内
	 [C] :?

 >リピーター(3)
 stdin:3:アサーションに失敗しました!
スタックトレースバック:
	 [C]:関数 'assert'内
	 stdin:3:関数 'repeater'内
	 stdin:1:メインチャンク内
	 [C] :?


すでにポイントに近づいていますが、あまり明確ではありません。



明快さのために戦う



エラーメッセージを改善してみましょう。



function repeater ( n, message ) <br/>

if type ( n ) ~ = "number" then <br/>

error ( <br/>

"bad n type: expected `number', got `" .. type ( n ) <br/>

2 <br/>

) <br/>

end <br/>

if type ( message ) ~ = "string" then <br/>

error ( <br/>

"bad message type: expected `string', got `" <br/>

.. type ( message ) <br/>

2 <br/>

) <br/>

end <br/>

<br/>

for i = 1 , n do <br/>

print ( message ) <br/>

end <br/>

end






エラー関数の2番目のパラメーターは、スタックトレースに表示する呼び出しスタックのレベルです。 今では「責める」のは私たちの機能ではなく、それを呼び出した人です。



エラーメッセージの方がはるかに優れています。



 >リピーター(3、 "foo")
 foo
 foo
 foo

 >リピーター( "foo"、3)
 stdin:1:bad n type:期待される `number '、got` string'
スタックトレースバック:
	 [C]:関数「エラー」
	 stdin:3:関数 'repeater'内
	 stdin:1:メインチャンク内
	 [C] :?

 >リピーター(3)
 stdin:1:不正なメッセージタイプ:期待される `string '、got` nil'
スタックトレースバック:
	 [C]:関数「エラー」
	 stdin:6:関数 'repeater'内
	 stdin:1:メインチャンク内
	 [C] :?


しかし今では、エラー処理は関数の5倍の有用な部分を占めています。



簡潔さのために戦う



エラー処理を個別に実行します。



function assert_is_number ( v, msg ) <br/>

if type ( v ) == "number" then <br/>

return v<br/>

end <br/>

error ( <br/>

( msg or "assertion failed" ) <br/>

.. ": expected `number', got `" <br/>

.. type ( v ) .. "'" ,<br/>

3 <br/>

) <br/>

end <br/>

<br/>

function assert_is_string ( v, msg ) <br/>

if type ( v ) == "string" then <br/>

return v<br/>

end <br/>

error ( <br/>

( msg or "assertion failed" ) <br/>

.. ": expected `string', got `" <br/>

.. type ( v ) .. "'" ,<br/>

3 <br/>

) <br/>

end <br/>

<br/>

function repeater ( n, message ) <br/>

assert_is_number ( n, "bad n type" ) <br/>

assert_is_string ( message, "bad message type" ) <br/>

<br/>

for i = 1 , n do <br/>

print ( message ) <br/>

end <br/>

end






これはすでに使用できます。



assert_is_ *のより完全な実装はこちら: typeassert.luaです。



メソッドを操作する



メソッドの実装をやり直します。



foo = { } <br/>

function foo:setn ( n ) <br/>

assert_is_table ( self, "bad self type" ) <br/>

assert_is_number ( n, "bad n type" ) <br/>

self.n_ = n<br/>

end






エラーメッセージは少しわかりにくいように見えます。



 > foo.setn(3)
 stdin:1:不良な自己タイプ:予想される `table '、get` number'
スタックトレースバック:
	 [C]:関数「エラー」
	 stdin:5:関数 'assert_is_table'内
	 stdin:2:関数 'setn'で
	 stdin:1:メインチャンク内
	 [C] :?


メソッドの呼び出し時にコロンではなくピリオドを使用するエラーは、特に経験の浅いユーザーにとって非常に一般的です。 実践は、自己をチェックするためのメッセージでは、それを直接指す方が良いことを示しています。



function assert_is_self ( v, msg ) <br/>

if type ( v ) == "table" then <br/>

return v<br/>

end <br/>

error ( <br/>

( msg or "assertion failed" ) <br/>

.. ": bad self (got `" .. type ( v ) .. "'); use `:'" ,<br/>

3 <br/>

) <br/>

end <br/>

<br/>

foo = { } <br/>

function foo:setn ( n ) <br/>

assert_is_self ( self ) <br/>

assert_is_number ( n, "bad n type" ) <br/>

self.n_ = n<br/>

end






これで、エラーメッセージは可能な限り明確になりました。



 > foo.setn(3)
 stdin:1:アサーションに失敗しました:bad self(got 'number');  `: 'を使用
スタックトレースバック:
	 [C]:関数「エラー」
	 stdin:5:関数 'assert_is_self'内
	 stdin:2:関数 'setn'で
	 stdin:1:メインチャンク内
	 [C] :?


機能の面で望ましい結果を達成しましたが、それでもユーザビリティを向上させることは可能ですか?



使いやすさを向上



各引数のタイプをコードで明確に確認したいと思います。 これで、タイプは関数名assert_is_ *に接続され、あまり区別されなくなりました。



次のように書くことができる方が良いです。



function repeater ( n, message ) <br/>

arguments ( <br/>

"number" , n,<br/>

"string" , message<br/>

) <br/>

<br/>

for i = 1 , n do <br/>

print ( message ) <br/>

end <br/>

end






各引数のタイプが明確に強調表示されます。 assert_is_ *を使用するよりも少ないコードで済みます。 この説明は、 Old Style Cの関数宣言に似ています (K&Rスタイルとも呼ばれます)。



void repeater ( n , message ) <br/>

int n ; <br/>

char * message ; <br/>

{ <br/>

/* ... */ <br/>

}






しかし、ルアに戻ります。 これで目的がわかったので、これを実現できます。



function arguments ( ... ) <br/>

local nargs = select ( "#" , ... ) <br/>

for i = 1 , nargs, 2 do <br/>

local expected_type, value = select ( i, ... ) <br/>

if type ( value ) ~ = expected_type then <br/>

error ( <br/>

"bad argument #" .. ( ( i + 1 ) / 2 ) <br/>

.. " type: expected `" .. expected_type<br/>

.. "', got `" .. type ( value ) .. "'" ,<br/>

3 <br/>

) <br/>

end <br/>

end <br/>

end






何が起こったのか試してみましょう:



 >リピーター(「バー」、3)
 stdin:1:不正な引数#1 type:期待される `number '、got` string'
スタックトレースバック:
	 [C]:関数「エラー」
	標準入力:6:関数の「引数」
	 stdin:2:関数 'repeater'内
	 stdin:1:メインチャンク内
	 [C] :?

 >リピーター(3)
 stdin:1:不正な引数#2 type:期待される `string '、got` nil'
スタックトレースバック:
	 [C]:関数「エラー」
	標準入力:6:関数の「引数」
	 stdin:2:関数 'repeater'内
	 stdin:1:メインチャンク内
	 [C] :?


短所



カスタムエラーメッセージは紛失しましたが、それほど怖くはありません。どの引数について話しているかを理解するには、その数で十分です。



この関数には、呼び出し自体の正当性に対する十分なチェックがありません-偶数の引数が渡され、すべての型が正しいという事実。 読者は、これらのチェックを自分で追加するように招待されています。



メソッドを操作する



メソッドのオプションは、さらにselfをチェックする必要があるという点でのみ異なります。



function method_arguments ( self, ... ) <br/>

if type ( self ) ~ = "table" then <br/>

error ( <br/>

"bad self (got `" .. type ( v ) .. "'); use `:'" ,<br/>

3 <br/>

) <br/>

end <br/>

arguments ( ... ) <br/>

end <br/>

<br/>

foo = { } <br/>

function foo:setn ( n ) <br/>

method_arguments ( <br/>

self,<br/>

"number" , n<br/>

) <br/>

self.n_ = n<br/>

end






関数の* arguments()ファミリーの完全な実装は、 args.luaで見ることができます。



おわりに



Luaで関数の引数をチェックする便利なメカニズムを作成しました。 これにより、予想される引数のタイプを視覚的に設定し、渡された値のコンプライアンスを効果的に検証できます。



assert_is_ *に費やされる時間も無駄になりません。 関数の引数は、型を制御する必要があるLuaの唯一の場所ではありません。 assert_is_ *ファミリーの関数を使用すると、このような制御がより視覚的になります。



代替案



他の解決策があります。 Lua-users wikiLua Type Checkingを参照してください。 最も興味深いのは、 デコレータを使用しソリューションです。



random = <br/>

docstring [ [ Compute random number. ] ] ..<br/>

typecheck ( "number" , '->' , "number" ) ..<br/>

function ( n ) <br/>

return math.random ( n ) <br/>

end






Metaluaに変数タイプを記述するためのタイプ拡張機能が含まれていますdescription )。



この拡張機能を使用すると、次のことができます。



- { extension "types" } <br/>

<br/>

function sum ( x :: list ( number ) ) :: number<br/>

local acc :: number = 0 <br/>

for i = 1 , #x do acc = acc+x [ i ] end <br/>

return acc<br/>

end






しかし、これはまったくLuaではありません。 :-)



All Articles