現在、5月25日から始まる技術書典に向けてVerse本の紙版リリースの準備を進めているのですが、無限に続く寒暖差のせいで体調が思わしくなくピンチでやばばです。頑張ります。
今回はそのVerse本に追記しようと思っているネタです。
問題編
キッカケは、ごろ助さんのこちらのツイートでした。
公式ドキュメントの「関数>関数ボディ」にある2個のサンプルコードがコンパイルを通らないという話。このページの「関数ボディ」の項目でdecidesの再確認しようかと思ったけど…
— ごろ助 / Gorosuke (@GorosukeUEFN) 2024年5月5日
false?の部分で「intを返してほしいのにlogicが返ってます」だし、戻り値がvoidの場合もreturnだと「returnは失敗コンテキストで許可されてません」となる(こっちの場合はfalse?は正常に動作する)… pic.twitter.com/PVaMW8B8s4
https://dev.epicgames.com/documentation/ja-jp/uefn/functions-in-verse#%E9%96%A2%E6%95%B0%E3%83%9C%E3%83%87%E3%82%A3dev.epicgames.com
確かに、サンプルコードにある下記の2つの失敗許容関数はコンパイルが通りません。
Find(X:[]int, F(:int)<decides>:void)<decides>:int = for (Y:X, F(Y)): return Y false? AnyOf(X:[]int, F(:int)<decides>:void)<decides>:void = for (Y:X, F(Y)): return false?
「公式ドキュメントのサンプルコードが動かないってどういうこと!?」という感じではありますが、ドキュメントがメンテされていないなどの理由でサンプルコードがコンパイルが通らないという事態は(残念な話ではあるものの)Verseに限らずよくある話だったりします。
なので、これも少し修正すればいけるだろうと思ったのですが、実はこれはVerseの公開されていない言語仕様が関係していて、そう簡単には解決出来ない物だったのでした。
試行錯誤編
まずこれらのサンプルコードは「失敗許容関数は常に<transacts>
指定が必要」「失敗許容関数の呼び出し時は丸括弧ではなくカド括弧でなければならない」「全てのフローは戻り値に指定した型を返さねばならない」という文法仕様を満たしていません*1。まずこれに対応します。
Find(X:[]int, F(:int)<transacts><decides>:void)<transacts><decides>:int = for (Y:X, F[Y]): return Y false? return 0 AnyOf(X:[]int, F(:int)<transacts><decides>:void)<transacts><decides>:void = for (Y:X, F[Y]): return false?
これにより、複数発生していたコンパイルエラーが下記の一個まで減りました(つまり、これが元凶という事です)。
Explicit return out of a failure context is not allowed. 失敗コンテキストから明示的にreturnすることは許されない。
しかし、このエラーが良く分からない。returnが記述出来ないコンテキストってなんだ?
ループを使うのが問題なのかもと色々試した結果、下記の様なシンプルなコードですらエラーになることがわかりました。
#これはNG Test01()<transacts><decides>:void = if: 1=1 then: return #Explicit return out of a failure context is not allowed. #これだとOK Test02()<transacts><decides>:void = if: 1=1 then: return else: return
上記のコードは挙動が同じです*2。にも関わらずTest01[]
だけコンパイルエラーになる。こうなっちゃうともう分かりません。お手上げです。
解決編
正直この時点では、コンパイラのバグだろうと考えていました。しかし、色々調べていて、土屋の知らない言語仕様がある事に気付きました。
結論から言いますと、エラーメッセージにあった「失敗コンテキストから明示的にreturnすることは許されない」というのは、Verseのれっきとた言語仕様だったのです。ここで言う「明示的なreturn」というのは、スコープの途中でreturn
やbreak
によってスコープを脱出する処理を指します。
この言語仕様は、ドキュメントのどこにも書いてありませんが*3、フォーラムで開発サイドが明言していました。 forums.unrealengine.com
You can’t return explicitly from a failable context. 失敗コンテキストでは明示的なreturnが出来ません。
なぜこんな仕様になっているかについては後述しますが、例えばif
式やfor
式の条件部(これらは失敗コンテキストです)の中でreturn
を書く事は普通はないでしょう。なので、このルールを破る事は滅多にないと思います。
一方、失敗許容関数はコードブロック全体が失敗コンテキストです。先ほどのルールと合わせると「失敗許容関数の中では明示的なreturnが出来ない」という事になります。もうお分かりかと思いますが、今回のコンパイルエラーはこれが原因だったのです。
「<decides>
指定子を付与した関数内では明示的なreturnが出来なくなる」というのは、直感的でないにも程がありますが、仕様通りではあるので、こういう物だと割り切るべきでしょう。
最初のツイートの話に戻ると、ドキュメントのFind[]
/AnyOf[]
はどちらもサンプルコードが間違っていたという結論になります。別案を以下に示します(Find[]
の場合)。
Find(X:[]int, F(:int)<transacts><decides>:void)<transacts><decides>:int = var RetInt:?int = false for (Y:X, F[Y], not RetInt?): set RetInt = option{Y} RetInt?
ちなみに、明示的なreturnが記述されていたとしても、そのreturnを削除してもフローが変わらない場合はエラーになりません。先に挙げたTest02[]
関数がエラーにならないのはその為です。これは恐らく、コンパイラがフロー解析してチェックしているのだと思われます*4。
補足:何故失敗コンテキスト内で明示的なreturnが許されないのか
さて、「失敗コンテキスト内では明示的なreturnができない」というルールはなぜ存在するのでしょうか。
開発サイドの説明によれば、Verseでは将来的に、失敗コンテキスト内の処理を、効率化のために(挙動が変わらない範囲において)順序を入れ替えて実行出来るようにする予定*5で、明示的なreturnが含まれるとその構想が崩れてしまう(だから明示的なreturnを禁止している)との事でした。
おわりに
いやーしかし知らんかったこんなルールw。毎日学びがあって楽しいな!!!
フォーラムでこの事が指摘されたのは2023年5月(ちょうど一年前)で、それ以来ドキュメントは更新されていない事になります。とはいえ、先述したようにドキュメントがメンテされないのはよくあることなので、自衛するしかないですね……><
宣伝
Verse言語の言語仕様解説同人誌をPDFで頒布しています。よろしければどうぞ。