Minecraftとタートルと僕

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

sleep実装からイベントを学ぶ(7)-Too long without yieldingエラーの解決方法2

エラーを避けるまた別の方法

前回の記事で、「Too long without yielding」を避けるために定期的に「sleep」を入れようというお話をしました。

しかし実は、これ以外にもエラーを避ける方法があります。
(これに気づいたのはつい最近なんですよね。これを踏まえて、これまでのプログラムを修正するかどうか悩み中)

Turtle APIを使うとエラーが起きない

まず、次のプログラムを実行してみましょう。

-- タートルの選択スロットを1と2交互に切り替える。延々と。いつまでも。
while true do
  turtle.select(1)
  turtle.select(2)
end

これを実行すると、いつまでたっても「Too long without yielding」エラーが発生しません。
また「turtle.select()」だけでなく、Turtle APIの他の関数を使ってもエラーが生じません。
なぜだと思いますか?

エラーが起きない理由

前回のお話を読んでいるなら、なんとなく想像つきますよね?
「Turtle API自体が、コルーチン含んでいるんじゃね?」

結論から言うと正解です。

証拠を見せましょう。
「ComputerCraft1.53/lua/rom/apis/turtle/turtle」というファイルを見てみましょう。
(CCコンピュータ側から見るなら、/rom/apis/turtle/turtle です。)

以下に重要な部分を抜粋しました。

local function waitForResponse( _id )
	local event, responseID, success
	while event ~= "turtle_response" or responseID ~= _id do
		event, responseID, success = os.pullEvent( "turtle_response" )
	end
	return success
end

local function wrap( _sCommand )
	return function( ... )
		local id = native[_sCommand]( ... )
		if id == -1 then
			return false
		end
		return waitForResponse( id )
	end
end

注目すべきは、上から4行目にある「os.pullEvent( "turtle_response" )」ですね*1
waitForResponse()関数は、os.pullEvent()を含んでいます。
そしてそのwaitForResponse()関数は、wrap関数で使われています。
wrapは、その名のとおり*2、Turtle APIの「ほとんど」の関数で自動的に呼ばれます。

だからTurtle APIの関数を定期的に使う限り、「Too long without yielding」エラーが生じないのです。

でもなぜTurtleAPIがos.pullEventを必要とするの?

os.pullEvent( "turtle_response" )は、"turtle_response"イベントを拾い上げています。
非公式CC wikiのイベント一覧から、このイベントに関する説明を引用しましょう。

第1返値 第2返値 第3返値 説明
"turtle_response" コマンドID(数値) コマンドの成否(ブーリアン) カスタムコマンドが実行された

つまり、turtle.select()のようなコマンドが成功したかどうかの成否を返すイベントのようですね。

よって、前述のwrap関数を大まかに解説するならば、

  • Turtle APIの各関数を実行すると、
  • コマンドを実行(wrap関数中、nativeうんぬんの部分)
    • コマンドが正規のものであれば、コマンドIDを返す
    • そんなコマンドそもそも実行できないよという場合に-1というIDを返す?(おそらく、turtle.select(100)などのような明らかに実施不能な場合に返すと思われる)
  • コマンドIDが-1なら失敗(false)を返す。
  • そうでないなら、waitoForResponse(id) を実施して、その結果を返す。

waitoForResponse(id)の解説

  • 実施したコマンドが実際に成功したかどうかを"turtle_response"として受け取る
  • もっと正確に言うと、ここでLuaの制御を一旦中止して制御をJava側に戻し、Java側で"turtle_response"イベントが生じたらLuaの処理を再開する。
  • なおwhileループの理由は、ここで処理したいコマンドであるか正確に判断するためです。違うコマンドのイベントだったら、またos.pulllEvent()して、制御をJavaに戻します。

つまりどういうことだってばよ!

Turtle APIを使ってタートルに何かをさせるとき、成功したらtrue、失敗したらfalse を返すという原則があります。
たとえば、turtle.suckUp()がtrueを返すかどうかで、アイテムゲットに成功したかどうかがわかりますよね。

この成功・失敗を正確に判断させるために、wrapから始まる一連の関数を使っているわけです。

  • 「命令がそもそもおかしくないか」という判断をwrap関数部分で行う、
  • そして「実際にその命令を実行して成功したかどうか」という部分を"turtle_respense"イベントにより判断する

私の環境はCC1.53なので、この程度のシンプルな処理ですが、CC1.56以降は、単純にtrue/falseだけじゃなく、失敗した理由までエラーメッセージで返してくれるようですね。
おそらく、CC1.56以降では、このwrap関数あたりで、エラーメッセージに関する処理を行っているのではないかと予想しています。

そのうち時間があるときに、調べてみようかな。

まとめ

前回のまとめに追記しますね。

  • CCでプログラム組むときには、定期的にコルーチンを呼びな!
  • さもないと、「Too long without yielding」と怒られるぞ
  • コルーチンが何なのかわからなくても大丈夫。os.pullEvent()のことだから。
  • 別に俺のプログラムでイベント処理する気はないんだけど ←だったらsleep使いな!
  • ええええ、sleep使ったらプログラムの動作が遅くなるじゃん ← sleep(0)使え! 0.05秒くらい我慢しなさい。
  • Turtle APIを定期的に使うとこのエラーは起きないのよ。さすがタートル君!!(ただし例外あり) 
  • 定期的にと言うけれど、実際にどのくらいの時間ごとなのか・・・・・・。体感では5秒くらい?

さらに補足

実はこのTurtleAPI以外にも、wrapしてos.pullEvent()を使うAPIはあります。以下にネタ元である、CC公式Wikiのcoroutineページから、「pullEvent()をwrapしているAPI一覧」を抜粋します。

  • rednet.receive blocks until either a rednet_message event or (if a timeout was requested) a matching timer event occurs.
  • gps.locate blocks until either a number of modem_message events or (if a timeout was requested) a matching timer event occurs.
  • http.get blocks until an HTTP success event or HTTP failure event occurs.
  • io.read and the built-in read functions block waiting for, presumably, either char events or key events.
  • os.sleep blocks until a matching timer event occurs.
  • textutils.slowPrint calls os.sleep.
  • textutils.pagedPrint blocks until a key event occurs for each page.
  • Most of the functions in the turtle API (with the exception of turtle.getItemCount, turtle.getItemSpace, and turtle.getFuelLevel) block waiting for a turtle response event.

結構ありますね。でも気軽に使えそうなのは、os.sleep()とTurtle APIぐらいですかね。残念。
そして、TurtleAPIの中でも、turtle.getItemCount()、 turtle.getItemSpace()、turtle.getFuelLevel()は例外のようですね。これら3つの関数は使っても、エラーを防げません。気をつけましょう。

*1:waitForResponse()関数の書き方が汚い。sleep()みたいにrepeat~until使えばいいのに。

*2:特定の関数を包みこんで少しだけ機能を付け加える関数のこと。慣習的にラッパーとかwrap関数と呼ばれる