MFGの@paramの話、なぜUIウィジェットはattributeなのか?
GPU用フィルタ記述言語、MFGのUIウィジェットの話。
MFGにはUIウィジェットというものがある。 スライダーとかチェックボックスとか位置指定とか向き指定とかだ。
以下は向きを取得するウィジェットの例。

これはプログラム言語上は以下のようになっている。
@param_f32 strength(SLIDER, label="強さ", min=3.0, max=300.0, init=10.0)
@param_f32 angle(DIRECTION, init=0.0)
言語仕様の詳細としてはリファレンスに解説がある。 リファレンス: アトリビュートと入力ウィジェット
だがここでは、仕様の詳細よりも、どうしてこうなっているのか、 という事を、プログラム言語の視点から雑談として書いてみたい。
簡単のため、上記のスライダーについてのみ話をする。以下の部分。
@param_f32 strength(SLIDER, label="強さ", min=3.0, max=300.0, init=10.0)
IRから見るとf32の変数strength
プログラム言語としては、ようするにIRがどうなるか、という事に着目するとわかりやすい。 上記のスライダーは、 プログラム言語のIR上では、streanghという変数で型はf32となる。
イメージとしては以下のような何か。
let strengh = ここに外からf32の値がセットされる
ようするにプログラム言語としてはletのようなものとして振る舞っている。 プログラム言語としては、この値がどのようにセットされるか、という事には関知しない。
これがスライダーであるのか他のウィジェットであるのか、そのラベルが何なのか、初期値や最大値、最小値が幾つなのかという事は気にしない。
気にするのは、以下のみだ。
- 変数名がstrength
- 型がf32
- 実行前に外からセットされる
ではそれ以外の指定は一体誰のためにあるのか?というと、UIアプリケーションである。
UIアプリケーションから見るとプログラムにセットしないといけないパラメータ
実際にMFGを実行するアプリケーションから見ると、 このプログラムは実行前にセットしていないといけないパラメータが幾つかあるように見える。
そしてそのパラメータをユーザーに問い合わせる時の付加情報として、上記のSLIDERとかlabelとかinitとかmaxとかminは使われる。
UIとしては、このプログラム全体に対しての付加情報として扱う(だからattributeとして実装している)。
スライダーの定義の、カッコの中はプログラムでは無くその外側が使う情報になっている(以下のSLIDER以下)
@param_f32 strength(SLIDER, label="強さ", min=3.0, max=300.0, init=10.0)
UIとしては、以下の情報を元にした入力をユーザーに問い合わせて、
- SLIDER
- labelが「強さ」
- min, max, initがそれぞれ指定された値
その結果をプログラム実行に先立ってセットしなくてはいけない。
このように、param文は、MFGとUIの情報を半分ずつ持ち、UIとプログラムの境界線となっている。
paramの中はexpressionでは無い
細かい話となるが、このparamはプログラムに先立ちセットされるものなので、 この中は実行はされない。 だからlabelやminなどの値は全て「式では無い」。
だから変数なども使えないし、加減乗除の二項演算も使えない。
それらが存在しないので、パースのルールも大きく異なったものになっている。
余談になるが、MFGの型システムには文字列型は存在しない。 このlabelは文字列の値を保持しているが、これはあくまでアトリビュートとしてプログラム全体に付随するメタ情報として存在するもので、 たとえば変数に入れる方法は無いし、文字列の値を返す式を書く方法も存在しない。
attributeのパースルールはそれ以外の式と大きく異なっている。
例えば、paramでは無いが @title は、以下のように関数呼び出しのカッコが無い。
@title "これはタイトルです"
これはMFGの他の部分とは大きく異なる文法となっている。
これをどれだけMFGのシンタックスと揃えるかは結構悩んだ所だ。
結局パーサーの中心となる式が無い事から、あまり流用出来る部分が無い事を思うと、 実装としては揃える意義はあまり無い。 だからこれはどちらが仕様として望ましいか、というだけの話となる。
一貫性がある方が学習は容易かもしれない。
一方でMFGのシンタックスは、本格的なプログラム言語であるが故に、複雑な式に耐えうるようなしっかりしたものになっている。 それはUIの情報だけに必要な文法としては冗長でもある。
MFGのattributeは、UIプログラムのマークアップなどを別に用意しない代わりに、 シンタックスをちゃんとUIの情報に特化させて簡潔に書ける事を優先し、 本体の言語との一貫性を犠牲にする事にした。
この辺は言語設計者としての選択となる。
先頭の引数と名前付き引数
先頭はEnumのようなシンボルとなっているが、MFGではEnumの名前は .ClampToEdge などのように . で始める事になっているので、ルールが異なっている。
また上にも書いたようにこれは式では無く型も無いので、Enum型とは違うものになっている。
さらに、MFGでは関数名で引数というのはある程度確定してvalidationが行われる。
だが、UIのウィジェットは同じ@param_f32 でもSLIDERの時もあればDIRECTIONの時もあり、
それぞれで必要な名前付き引数の部分が変わる。
これはMFGの文法には無い機能だ。
UIのウィジェットとしては、先頭のシンボルでそのあとの名前付き引数として許されるものが変わる、というルールになっている。
@param_f32 hoge(「1. ここにウィジェットの種類」, 「2. ここの名前付き引数は1に対応して決まる」 )
このように、かなり特殊なパースルールとなっている。 式が無いおかげでアドホックなルールでも破綻しない単純なものしか来る事が出来ないので、 UIの記述の簡潔さと読みやすさを優先したルールとなっている。
まとめ
@param関連はUIとコードの境界の所にあるよ- コードからは変数だけで、残りの情報はUIが使うものだよ
- UIで使う部分のパースルールはMFGのほかの所とは結構違う独自のもので、読みやすさと書きやすさ優先だよ
おまけ: Colabのformsとの比較
MFGとは関係ないが、似たようなものは世の中に結構あるので、比較として見ておこう。 この手のものは機械学習界隈では割とあるが、一番似ているのはColabのforms関連機能だ。
text = 'value' # @param {type:"string"}
dropdown = '1st option' # @param ["1st option", "2nd option", "3rd option"]
text_and_dropdown = 'value' # @param ["1st option", "2nd option", "3rd option"] {allow-input: true}
text_with_placeholder = '' # @param {type:"string", placeholder:"enter a value"}
Colabでは、プログラム要素としては通常の変数で、UI的な情報はコメントで実現している。 paramの後はJSONで、配列の記述の問は簡潔に、 objectリテラルの時は冗長だが一貫性があり汎用的なルールとなっている。
コメントにいろいろな意味をもたせるのは、元の言語に手を入れずに追加の機能を実現出来るため、 割と使われるものでもある。 一方で勝手に自分で決めたルールでパースする事になるためルールは分かりにくい部分が出来てしまったりもして、 長期的にはメンテナンスの負担になる事も多い。
Colab(というよりJupyter Notebook)は、セルとう単位がUIとのinteractionの単位となっているので、 こうしたUIのひとまとまりを記述するのに都合が良い。 だから少し複雑なUIを書いていこうとすると可読性や生産性に問題がありそうなこうした単純なしくみでも、 セルという狭い範囲に限定すれば問題無い、という、Colabの特性をよく活かした仕様になっていると思う。
Colabのフォーム関連機能は、この範囲で出来る事は簡単にかけて、割と便利に使わせてもらっている。 あると無いとでは大違い。
一方でこの範囲で出来ない事をやろうとした瞬間に一気に面倒になって、もうちょっとなんとかならんかな〜、と思う事もある。 この「もうちょっと」の所を割り切ってあるのが、便利に使える秘訣だろうけれど。
UI記述は記述の簡潔さ、読みやすさと、ルールの一貫性やシンプルさがトレードオフの関係になる事が多い。 UIに関してはこのトレードオフの後者、つまりシンプルで一貫した、けれど冗長なものでの記述は、あまりうまく行かない印象がある。 かつて独自のXMLでそういったUI記述を強制するものは多かったが、あまり良い思い出は無い。 Colabのform関連の記述がオブジェクトリテラルだけで統一してい無いのは、その辺よく分かっているなぁ、という気がする。