土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#UEFN #Verse 失敗許容関数内では明示的なreturnが出来ないルールについて

 現在、5月25日から始まる技術書典に向けてVerse本の紙版リリースの準備を進めているのですが、無限に続く寒暖差のせいで体調が思わしくなくピンチでやばばです。頑張ります。

 今回はそのVerse本に追記しようと思っているネタです。

問題編

 キッカケは、ごろ助さんのこちらのツイートでした。

 公式ドキュメントの「関数>関数ボディ」にある2個のサンプルコードがコンパイルを通らないという話。

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」というのは、スコープの途中でreturnbreakによってスコープを脱出する処理を指します。

 この言語仕様は、ドキュメントのどこにも書いてありませんが*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で頒布しています。よろしければどうぞ。

s-games.booth.pm


*1:これ自体どうなんだとも思うが

*2:実際には、後述するようにコンパイラはこの2つのコードを区別します。

*3:もし見つけたら教えてください

*4:だったらTest01[]も通ってもいいじゃないかとも思うけどこれは解析に限界があるのだと思われる

*5:明言はされていませんが、並列処理化を想定しているのだろうと想像します。