Lua
eval
assert

Lua で eval 関数を作る際に、なぜ assert を使うのか(もしくは、load 関数の戻り値について)

More than 1 year has passed since last update.

概要

Lua で eval 関数を作る場合、load() を使用することはもちろんですが、assert() を併用している例を多く見かけます。ここでは、eval の基本的な説明と、なぜ assert を併用するのかについて解説を行います。

Lua での eval 基本編

文字列をプログラムとして評価する関数は、一般的に eval と呼ばれています(ちなみに evaluate という英単語は「評価する」という意味で、evaluation は「評価」という意味です)。Lua でも load 関数にて、この機能は提供されています。古い Lua では loadstring という関数名で提供されていたようです。

実際には、load に文字列を渡すだけでは実行してくれないので、次のようなコードを書く必要があります。

ex_load.txt
> a
nil
> load("a=1")()
> a
1

load の戻り値は関数であり、文字列として記述したプログラムを実行するためには、戻り値に () を付けて実行させる必要があるわけです。

ex_type_of_return_value_of_load.txt
> type(load("a=1"))
function

ネットで見かける eval の実装例

Lua での eval の実装例として、以下のようなコードをよく見かけます。

eval.lua
function eval(inStr)
    return assert(load(inStr))()
end

これは、単に load 関数を用いるだけでは、与えられた文字列にエラーが含まれている場合などにうまく対処できないからです。

例えば、"a=" という不完全な文字列を評価する場合、いつもの "unexpected symbol near..." というエラーメッセージが出力されて欲しいものです。しかし、load("a=")() を実行すると、"stdin:1: attempt to call a nil value" というメッセージが出力されます。

これは、"a=" というプログラム片だから、nil value 云々というメッセージが出力されたわけではありません。どのようなエラーを含む文字列でも、全てこの nil value 云々というメッセージが出力されます。これは eval 関数の挙動として、あまり良いものではありません。可能であれば、適切なエラーメッセージが出力されて欲しいものです。

load 関数の戻り値

load 関数の仕様は、公式サイトの https://www.lua.org/manual/5.3/manual.html#pdf-load で確認できます。それによると、引数として与えられた文字列にエラーがない場合は関数としてコンパイルされたチャンクが返され、そうでない場合(つまりエラーがある場合)は nil とエラーメッセージが返される、とあります。実際に、引数にエラーが含まれない場合、およびエラーが含まれる場合の挙動は以下のようにして簡単に確かめられます。

ex-load-without-with-error.txt
> select("#",load("a=1")) -- num of return values in case of no error
1
> select("#",load("a=")) -- num of return values in case of errors
2

> r,s=load("a=") -- in case of the argument with errors
> r
nil
> s
[string "a="]:1: unexpected symbol near <eof>

引数として与えられた文字列(=評価される文字列)にエラーが含まれる場合、load 関数は常に nil を第 1 戻り値として返します。そのため、load("a=")() のように、単に load 関数の呼び出し直後に () を付けて実行する実装では、エラーが含まれる文字列の場合、常に "stdin:1: attempt to call a nil value" というエラーメッセージが表示されることとなります。

assert の活用

上で見たように、load 関数は評価する文字列にエラーがある場合に nil を第 1 戻り値として返します。であれば、素直に eval を実装すると、例えば、以下のようなコードが考えられます。

rustic-eval.lua
function eval(inCodeStr)
    local r,s=load(inCodeStr)
    if r~=nil then
        return r()
    else
        error(s)
    end
end

もちろん、これでも動くわけですが、assert を使うと、より簡潔に記述できます。Lua のマニュアル https://www.lua.org/manual/5.3/manual.html#pdf-assert によると、assert は 2 つの引数をとり、第 1 引数が nil でなければ引数全てを返し、そうでない場合は第 2 引数をエラーオブジェクトとして error 関数に引き渡します(つまり、第 2 引数がエラーメッセージとして表示されます)。

これは、load 関数にとっては非常に都合の良い仕様となっています。load 関数で評価すべき文字列にエラーが含まれる場合は、nil とエラーメッセージが assert 関数に引き渡され、結果として、error 関数にエラーメッセージが引き渡されて実行されます。もし、load 関数に渡される文字列にエラーが含まれない場合には、第 1 引数は nil ではなく関数となります。そのため、assert 関数の戻り値は load 関数の戻り値と等しくなり、assert 関数の結果を関数として実行することは、与えられた文字列をプログラムとして実行することとなります。つまり、上で示した素朴な実装と同等の処理は、先に上げた次のコード(再掲)のように書けます。

eval.lua(再掲)
function eval(inStr)
    return assert(load(inStr))()
end

まとめ

この文書では、eval 関数について簡単な説明を行い、Lua でも load 関数にて同等の処理ができることを示しました。ネットなどでは load と assert を組み合わせた例が多くみられるため、load 関数の仕様と assert 関数の仕様を説明し、assert 関数を活用することにより、簡潔に eval 関数を記述できることを示しました。

以下、個人的な感想ですが、Lua のライブラリはよく考えられているなぁ、と再認識しました。