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

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

#UEFN #Verse 作用指定子(effect specifier)のv30.0新仕様について解説

 Verseでは、ライブラリの実装者が各APIが持つ制約を明示する為に、関数名に「作用指定子(effect specifier)」を付与できます。これによって、その関数が副作用を持つかどうかや、環境依存性があるどうかなどをインターフェイスレベルで規定できます。

 先に言っておくと、現時点のVerseでは作用指定子を設定する意味はほぼありません。乱暴ですが標準ライブラリ実装者(つまりEpic Games社員)だけ理解していればいい仕様と言えます。transacts指定子だけは話が違うのですが、これは特殊な作用指定子で、しかも、将来的には廃止される(!)予定になっています。

 では作用指定子はなんのためにあるのかと言いますと、将来的なコンパイル時最適化のためです。特に、Verseが(現在の「並行」処理ではなく)「並列」処理を実行出来るようになった際に、複数のタスクを効率的にスケジューリングするために作用指定子が参照されるようになります*1

 そんなわけで、現時点ではtransacts指定子のみ把握していればいいわけですが、せっかくなので現時点で作用指定子について分かっている事をまとめておきたいと思います。

 作用指定子はv30.00から仕様が大きく変化(というかほぼ仕様を再定義)していまして、まず新仕様を説明したのち、元々はどうなっていたかに触れます。また、現状では最も我々に関係のあるtransacts指定子が、何故将来的に廃止されるのかについても触れたいと思います。

 なお、上記の様な状況もあって、作用指定子については公式ドキュメントでも解説があったりなかったり古いままだったり間違ってたりしていまして、ここでは土屋による大胆な推測込みの「恐らくこういう仕様にしたいのだろう」という解説を行います。場合によっては実際の挙動とも一致しないかもなので、予めご容赦下さい。

余談:"effect specifier"の訳語について

 余談ですが、公式ドキュメントや拙著のVerse本では"effect specifier"の訳語を「エフェクト指定子」としています。しかし、ここでの"effect"は、関数型言語における「副作用(side effect)」と対になる用語「効果/作用(effect)」に由来すると思われるので、このブログでは以後「作用指定子」とします。Verse本の紙版をリリースする前に気付きたかった……。ぐぬぬ、失敗した……!

作用指定子

 現在用意されている作用指定子は<computes><reads><writes><allocates>の4種類です。これらの作用指定子は組み合わせて使用出来ます。ただし、現状では<writes><allocates>については公式ライブラリ内で使用例がありません。

 また、扱いが特殊ですが、<transacts>も作用指定子に含まれます。

computes指定子

 computes指定子は、指定した関数が「副作用(side effect)」を持たない事を示します。関数型言語では、関数が入力に応じた出力を返す処理を「作用」、それ以外に実行環境の状態を参照/更新する処理(例えば、グローバル変数を書き換えるとか)を「副作用」と呼び、区別しています。

 Verseでは、以下の3つの制約が守られる事を「副作用が無い」としています*2

  • read制約:スコープ外の値を参照しない。
  • write制約:スコープ外の値を更新しない。
  • allocate制約:オブジェクトを生成しない(≒追加メモリを確保しない)。

 副作用が無い関数は、入力に対して常に同じ出力を返します。例えば、sinやcosなどの数学関数は、入力に対して常に同じ出力を返すので、副作用が無い関数です。実際、verseの数学関数関連のAPIはcomputesが指定されています。

 副作用が無い関数は、他の処理から独立しているため、並列処理時に最適化されやすくなります。

 ちなみに、更新だけでなく、参照するだけでも「副作用」と呼ぶ事に違和感があるかもしれませんが、ある値を参照する事によって状態が変わる事もありますし(getプロパティとか)、参照するたびに参照先の値が変わる場合、それが出力にも影響します。

reads指定子

 reads指定子は、指定した関数がクラスメンバ変数やモジュールスコープ変数を参照する可能性がある事を示します。

 reads指定子は、computes指定された関数の制約を緩和する為に記述します。そのため、<reads>は常に<computes>と併記されます*3

 <computes><reads>と指定することで「この関数はcomputes制約を持つが、スコープ外変数への参照は許容する」という事を示します。また、これは同時に、この関数が入力に対して常に同じ出力を返すわけではない事を示します。

 ちなみに、実際のVerseのAPIでは、数学関数はいずれも<computes><reads>になっています。この理由についても後ほど説明します。

writes指定子

 writes指定子は、指定した関数がクラスメンバ変数やモジュールスコープ変数を更新する可能性がある事を示します。readsと同じく、computesとセットで使用します。

 v31.20現在、VerseAPIでwrites指定された関数はありません。

allocates指定子

 allocates指定子は、指定した関数が、オブジェクトをインスタンス化する為に、追加でメモリを確保する可能性ある事を示します。reads/writesと同じく、computesとセットで使用します。

 「あるオブジェクト(プリミティブ型も含む)をインスタンス化する」というのは、そのオブジェクトの為にメモリ空間上にメモリ領域を確保する処理なわけで、これも副作用に当たります。そのため、computesのみを指定した(allocates無しの)関数は、ローカル変数を定義する事もできません。

 v31.20現在、VerseAPIでallocates指定された関数はありません。

transacts指定子(特殊な作用指定子)

 transacts指定子は扱いが特殊で、作用指定子としては<reads><writes><allocates>のシンタックスシュガー*4として振る舞います。

 また、transacts指定子を付与した関数はロールバック可能になります。

 ただし、このtransacts指定子は将来的に廃止予定になっています。これについは後述します。

作用指定子の旧仕様

 v30.00より前の仕様では、作用指定子はconverges/computes/varies/transactsの4種類が用意されていました。convergesが最も制約が強く、順に制約が緩和されるという物でしたが、この分類は当初から正常に機能していませんでした。

 以下軽く解説しておきます。旧仕様に作用指定子については、拙著のVerse本で詳しく解説しているので、そちらも参照してください。

  • converges:もっとも制約の強い指定子。ユーザーは定義出来ない。
  • computes:新仕様に引き継がれた。制約レベルはconvergesとほぼ同じ。
  • varies:新仕様では<computes><reads>に置き換えられた*5
  • transacts:新仕様に引き継がれた

 converges指定子は新仕様でも使用されていますが、付与されているのはcolor型を生成/操作する一部の関数に限られています。本来数学関数群はconverges指定が正しいのですが、実際には<computes><reads>になっています。これは、VerseAPIの数学関数が内部的にC++ライブラリの同名関数を呼び出しており、その結果が実行環境が持つ精度によって出力が変わる可能性があるためです*6

 このような経緯から、現実問題としてconverges/computesを区別する理由が無いため、convergesは将来的に廃止されるだろうと思います*7

 varies指定子は、新仕様で表すと<computes><reads><writes>に相当する指定子です。旧仕様では、VerseAPIの関数に作用指定をする場合、converges/computeは制約が厳しすぎて付与できず、ほとんどがvariesになってしまい、これはこれで場合によっては制約が緩すぎてしまう問題がありました。これを解決するために新仕様が設計されたのだろうと思われます*8

ロールバック指定子としての<transacts>の話

 transacts指定子は、先述した作用指定子としての機能とは別に、指定した関数がロールバック可能である事を示します。というかむしろ、「関数をロールバック可能にする指定子」としての方が一般的かと思います。

 しかし、実はこれは変な話でして、Verseの設計思想からすれば、transacts指定子の有無など関係なく、全ての関数がロールバック可能であるべきですし、実際に未来のVerseではそうなっている予定です。

 では何故transacts指定が必要なのかと言いますと、それは現在のVerseがUEのBluePrintVM(以下BPVM)上で動いている事に起因しています。

 現在、VerseAPI上の多くの関数がBPVMの同名APIを呼び出しています。Verseのロールバック機構は、いつでもロールバック出来るように更新前の値を保持しているのですが、BPVMはその管理外にあります。そのため、BPVMを呼ぶAPIを含む処理は、ロールバックが出来ないのです。

 そこで、現状では折衷案として、transacts指定を使う事で「このスコープ範囲にはBPVM呼び出しが無いのでロールバック可能である」という事を明示する形にしているのです。  来る未来、VerseVMが完成してBPVMを使わなくなった暁には、transacts指定子は廃止され、任意の関数でロールバック可能になる予定です。

 ちなみに、ドキュメント上では「作用指定子の無い関数はno_rollback指定子が付与された物として扱う」という記述がありますが、このno_rollbackは、ここまで説明した様に言語仕様上は本来必要のないtransacts指定子を採用している為に作用指定子のルールが歪んでおり、その解決策として用意された苦肉の策だと考えて良いと思います。

 余談ですが、現在、失敗許容関数を定義する時に<decides><transacts>の2個の指定子を付与する必要があります。コーディングしている方は皆さん「<decides>だけ付与すれば<transacts>扱いになってくれても良いんじゃない?」と思われているかと思います。この不思議なルールは、上記の問題に起因しているのだと思われます。

おわりに

 この記事、当初は5月末に書き始めたのですが、そもそも作用指定子の仕様が入り組んでいて上手く纏めるのが難しく、また土屋が忙しくなった事もあって数ヶ月放置していました。

 ようやく時間が取れたので今回記事として書き上げられたのですが、やっぱり上手く説明仕切れなかった印象があります。数カ所実機検証した上で加筆したい所もありまして、今後気付いた時にちょこちょこ直していくと思います。

参考リンク

dev.epicgames.com 公式ドキュメントの作用指定子の解説ページ。ただしこのページに掲載されているのは旧仕様です。

dev.epicgames.com v30.0アップデート時の新仕様の作用指定子の解説。ただしこのドキュメントはミスが多く(<reads>の説明の中に<varies>の説明が重複して掲載されているなど)、不完全な記述になっています。

……というわけで、新仕様について正しく纏められたページは2024/9/25現在存在しません。

宣伝

Verse言語の言語仕様解説同人誌をPDFで頒布しています。よろしければどうぞ。

s-games.booth.pm


*1:恐らくは

*2:土屋定義です

*3:コンパイルエラーにはならないが、reads指定子を単体で使用する用途は考えにくい

*4:syntax suger(糖衣構文)。コード記述の簡略化の為に用意されている構文の事

*5:新仕様でもシンタックシュガーとして使用出来る筈

*6:明文化されていませんが恐らく正しいと思われます

*7:土屋の推測です

*8:推測です