挑戦する
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 wikiのLua 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ではありません。 :-)