,
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ではありません。 :-)

Source: https://habr.com/ru/post/J76001/


All Articles