Minecraftとタートルと僕

PCゲームMinecraftのMOD「ComputerCraft」の情報を集めたニッチなブログです。

ComputerCraftの無線通信を使いこなそう(9) ―命令の有効性や成功・失敗を調べる

はじめに

  • f:id:hevohevo:20140320001500p:plain

前回の最後に、以下のような課題を挙げました。

命令したときに、命令の遂行に成功したのか失敗したのか、そして失敗したのならどのような原因で失敗したのか。これらのエラーが返ってくると、さらにわかりやすく使いやすくなりますよね。

この課題を今回と次回の2回に分けて、段階を踏んで実装してみましょう。

まず今回は、命令メッセージがそもそも有効なのかどうか、そして実行したら成功したのか失敗したのかについて、どのように判断したらよいか調べてみましょう。

なおここでは、以下の2つのレベルのエラーを区別して調べています。ご注意ください。

  • 指定した関数がそもそも存在しないときに発生する重大なエラープログラムが強制停止する。
  • 実行したら何らかの原因で失敗したときに発生する軽微なエラー。発生しても特に処理を書かなければそのままプログラムは続く
    • 第1返値としてfalse、第2返値としてその失敗原因を返すような比較的軽微なエラー。
    • たとえば関数turtle.down()は、燃料がなく移動できないときに第1返値false、第2返値"Out of fuel"を返す。

今回は、Luaの細かな仕様について調査・検討した内容となっています。 さほど興味のない方は、さいごの「まとめ」だけ読んでいただいてもかまいません。

loadstring()の返値について調査

これまでのプログラムでは、受け取ったメッセージ文字列をloadstring()を使って関数にコンパイルしていました。それではこの引数におかしな値を与えたらどうなるのでしょうか。

func = loadstring("turtle.select(1)")
print(type(func))

上のコードに以下のような文字列を当てはめて調べた結果が、以下の表になります。

引数文字列 funcの値 備考
turtle.select(1) function 実行可能な正確な関数文字列を与えると「関数」
aaa() function 存在しない適当な関数っぽい文字列を与えても「関数」
aaa nil そもそも関数でもない適当な文字列を与えると「nil」

まとめると、

とにかく関数っぽい文字列(末尾に「()」)を与えると関数になり、関数以外の文字列を与えるとnil、となります。

問題となるのは、aaa()のような実行不可能な関数っぽい文字列を与えても関数ができてしまう点ですね。つまりloadstring()だけでは、エラーを詳細に判別するのが難しいようです。

pcall()を詳細に検討する

それでは、出来上がった関数を実行するpcall()ならば細かく判別できるのでしょうか。このpcall()の機能は以前の記事で紹介済みですが、念のため再掲します。

  • pcall(f, arg1, arg2, ・・・)
    • pcall()は一言でいうと、「関数を実行してエラーが発生したらそれを捕まえて、falseとエラーメッセージを返す」関数です。
    • 引数1(無名関数): 実行したい関数を与えます
    • 引数2以降、その無名関数に与える引数をいくつでも
    • 返値1(boolean): ステータスコード。関数実行成功ならtrue、エラーが生じたらfalse。
    • 返値2以降: 関数実行成功なら関数実行による返値を、エラー発生時にはエラーメッセージを返す。
    • 実行例: pcall(func, arg1, arg2) は、 func(arg1, arg2)を実行してエラー発生を捕らえて処理を行います。

ポイントは、返値ですね。

簡単に言うと、一番目の返値のtrue/falseによって、プログラムが強制終了するような重大なエラーが発生したかどうかが判断できます。当然ながら、falseならエラー発生です。

しかし、逆にこの返値1がtrueだとしても、必ずしも関数の実行に成功するとは限りません。

たとえば、タートルが下に移動するturtle.down()関数を実行したときに、障害物があった、燃料が足りなかったなどの理由で実行に失敗したときには返値1としてtrueを返すと同時に、返値2としてfalse、返値3として失敗した理由を返します。

失敗した理由(返値3) 意味
Movement obstructed ブロックやmob、プレイヤーなどに妨害された
Out of fuel 燃料切れ
Too high to move ブロックを設置できる限界高度にいる
Too low to move ブロックを設置できる限界深度にいる

pcall()についてわかったこと

pcall()についてまとめます。

local results = {pcall(func)}
print(results[1])
print(results[2])
print(results[3])

上記のようなコードによって、pcall()の返値すべてをテーブルコンストラクタ{}によってテーブルresultsに代入できます。

以下は、funcの値によってどのような返値となるのかを調べた表です。

funcの値 results[1] results[2] results[3] 備考
関数turtle.up true true - 問題なく移動成功のとき
関数turtle.up true false "Out of fuel" 燃料不足により移動失敗
存在しない関数aaa() false "attempt to call nil" - そもそもそんな関数はない!

pcall()の3つの返値を使えばうまく失敗の種類を判別できそうですね。

そして当然のように存在する罠

これで判別は簡単だ! と油断してはいけません。次のコードを実行してみましょう。

状況としては、前述の燃料不足により移動失敗のエラーとなることを想定しています。

local func = loadstring("turtle.up()")
local results = {pcall(func)}
print(results[1])
print(results[2])
print(results[3])

そうすると、返値1(results[1])がtrueとなるのですが、2番目以降の返値が帰ってきません。

あるぅえー?

実は、関数loadstring()によってコンパイルした関数は、「明示的にreturn文を埋め込まなくては値を返さない」という性質があります。

local func = loadstring("return ".."turtle.up()")
local results = {pcall(func)}
print(results[1])
print(results[2])
print(results[3])

このようなコードであれば、引数2と引数3もしっかりと得られます(ちなみに..によって二つの文字列を連結します)。

この方式の限界について

なお、次のような関数を複数並べた文字列のときには"return "を頭につける方式は通用しません。

-- セミコロン「;」は行区切り記号。
local str = "turtle.turnRight(); turtle.forward(); turtle.turnLeft()"
local func = loadstring("return "..str)
local results = {pcall(func)}
print(results[1])
print(results[2])
print(results[3])

だって、"return turtle.turnRight(); turtle.forward(); turtle.turnLeft()" というのは、Luaの文法的に意味不明ですから。

そもそも、3つの関数のどれに注目して値を返すのかすら不明ですし。

だから、本プログラムでは、行区切り記号であるセミコロン「;」を目印に、そのような文字列には"return "をつけないことにします。

コラそこっ! 手抜きって言わない!!

まとめ

loadstirng() → pcall() という流れで文字列を関数として実行するとき、

  • pcall()の3つの返値のパターンを調べることで、重大なエラーの発生(そんな関数は無い)/関数実行して成功/関数実行して失敗(軽微なエラー発生)の判別ができる。
  • ただし、loadstring(文字列)で関数を生成するときに、"return turtle.up()"のように頭に"return "をつける必要がある。
    • この方式が通用しない場合もあるのでそのときはあきらめる:P

次回は、今回判明したこの2つの事項を元に、

命令したときに、命令の遂行に成功したのか失敗したのか、そして失敗したのならどのような原因で失敗したのか。これらのエラーが返ってくると、さらにわかりやすく使いやすくなりますよね。

という課題を実現できるプログラムを作りましょう。