MPE 1: Selectの改善
- Created: 2022-08-29 08:48:09
selectは出番が多く可読性が低く野暮ったいのでもっとマシにしたい。
ごま塩フィルタの例を考える
def result |x, y| {
let r = rand()
select( r < SALT_THRESHOLD, 0xffffffff,
select( (1.0 - r) < PEPPER_THRESHOLD, 0xff000000,
input(x, y)))
}
検討すべき事
- MFGではネストする機会も多く出番も多い
- VMではショートカットが無いし(条件と反対側の文も実行はされるが結果が使われないだけ)、GPUでもショートカットは見た目しか起こらない(ステップ数は掛かる)
- 配列の領域外アクセスとかは成立しない方でもやってはいけない
- でもVMを廃止すれば全ての処理系でショートカットする事になりそうなのでこの条件は緩めてもいいかも
- 必ず値は返す(else節は必ずある)
大きく方針は3つあると思う
関数呼び出し
- 利点: 実際に関数呼び出しなので落とし穴が無い
- 利点: 理解も容易
- 欠点: ネストすると見づらい
- 欠点: ネスト無くても条件と値の役割が違うのに引数上は区別がしづらい(から読みにくい)
if, else的シンタックスシュガーの導入
- 利点: 読みやすい
- 利点: 他の言語で馴染みがあるので分かりやすい
- 欠点: 他の言語と違い必ずどちらも実行される事、必ずelseがなくてはいけない事など、他の言語から予想されるのと違う部分があるので落とし穴になりやすい
ハテナとコロンの三項演算
- 利点: 馴染み深い
- 欠点: ネストに弱い
- 欠点: そんなに読みやすくもない
関数呼び出し
この場合は名前が検討事項になる。とりあえず思いつくのを列挙しておく。
- select: Halide由来で現状これだが、長いしやぼったい
- ifelse: Rはこれ。悪くないがちょっと長い来もする
- ifel: なんかこのくらいがいいんじゃないかな?という気もする
- cond: なにかで見た事ある気がする。名前つき引数と合わせて条件と値の区別ができないか?
condの例
def result |x, y| {
let r = rand()
cond( r < SALT_THRESHOLD,
t=0xffffffff,
f=cond( (1.0 - r) < PEPPER_THRESHOLD,
t=0xff000000,
f=input(x, y)))
}
一つの時はどうだろう。
def result |x, y| {
cond( rand() > 0.5, t=0xff000000, f=0xffffffff )
}
あんまりだなぁ。改行するとどうだろう?
def result |x, y| {
cond( rand() > 0.5,
t=0xff000000,
f=0xffffffff )
}
うーん。selectと大差無い気がする。
Rと同じifelseを試してみる。
def result |x, y| {
let r = rand()
ifelse( r < SALT_THRESHOLD, 0xffffffff,
ifelse( (1.0 - r) < PEPPER_THRESHOLD, 0xff000000,
input(x, y)))
}
selectよりは良い気がする。インデントを調整してみる。
def result |x, y| {
let r = rand()
ifelse( r < SALT_THRESHOLD,
0xffffffff,
ifelse( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
))
}
悪くない気はするな。
条件一つの例。
def result |x, y| {
ifelse( rand() > 0.5, 0xff000000, 0xffffffff )
}
これもアリには思える。
ifelseは少し長い。もっと短くできないか?ifeだと意味が分からないのでifelくらいにしてみよう。
def result |x, y| {
let r = rand()
ifel( r < SALT_THRESHOLD, 0xffffffff,
ifel( (1.0 - r) < PEPPER_THRESHOLD, 0xff000000,
input(x, y)))
}
なかなか良い気がする。インデントを調整してみる。
def result |x, y| {
let r = rand()
ifel( r < SALT_THRESHOLD,
0xffffffff,
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
))
}
一つのケース
def result |x, y| {
ifel( rand() > 0.5, 0xff000000, 0xffffffff )
}
こっちの方が好みだなぁ。
elif関数も作ってみると、シンタックスシュガーに近づかないか?
def result |x, y| {
let r = rand()
ifel( r < SALT_THRESHOLD,
0xffffffff,
elif( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
))
}
悪くない気もするが、elifという同名関数があるというのはややこしさもあるかもしれない。
どうせならさらに短く、ifeにしてみる。
def result |x, y| {
let r = rand()
ife( r < SALT_THRESHOLD, 0xffffffff,
ife( (1.0 - r) < PEPPER_THRESHOLD, 0xff000000,
input(x, y)))
}
def result |x, y| {
let r = rand()
ife( r < SALT_THRESHOLD,
0xffffffff,
ife( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
))
}
def result |x, y| {
ife( rand() > 0.5, 0xff000000, 0xffffffff )
}
思ったより悪くないな。ただちょっと暗号的か。
関数名の暫定的結論
- Rに合わせるならifelse
- 既存言語を気にしないならifel
なお、なにか良いシンタックスシュガーがあれば入れたい気はするが、関数呼び出しっぽさが明確なシンタックスシュガーは難しい。
if文的シンタックスシュガー
読みやすさではやはりシンタックスシュガーを入れたい気もする。
中括弧は省略したい。ifには必ずelseがつくのでdanglingの問題は無い。 kotlinはカッコが必須だが、使っているとまぁまぁかったるいんだよな。
kotlin的にカッコ必須にしてみる。
def result |x, y| {
let r = rand()
if (r < SALT_THRESHOLD)
0xffffffff
elif ((1.0 - r) < PEPPER_THRESHOLD)
0xff000000
else
input(x, y)
}
やっぱいまいちだよなぁ。改行を式の区切りとしてみる。
def result |x, y| {
let r = rand()
if r < SALT_THRESHOLD
0xffffffff
elif (1.0 - r) < PEPPER_THRESHOLD
0xff000000
else
input(x, y)
}
悪くない気がするが、少し条件の終わりが分かりにくいか。
python的に区切りにコロンを使う。
def result |x, y| {
let r = rand()
if r < SALT_THRESHOLD:
0xffffffff
elif (1.0 - r) < PEPPER_THRESHOLD:
0xff000000
else:
input(x, y)
}
これが一番見やすい気はするが、言語の他の所にコロンが無いのにここで唐突に出てくるのが一貫性はいまいち。
一行に書きたいケースではifelse関数を使えば良いので考えなくてよかろう。これはあくまでネストするケース専用のシンタックスとしたい。
パターンマッチ的な記法だとどうだろう?
match {
r < SALT_THRESHOLD -> 0xffffffff
(1.0 - r) < PEPPER_THRESHOLD -> 0xff000000
else -> input(x, y)
}
そんなに見やすい気もしないなぁ。インデントを変えるとどうだろう?
match {
r < SALT_THRESHOLD
-> 0xffffffff
(1.0 - r) < PEPPER_THRESHOLD
-> 0xff000000
else
-> input(x, y)
}
C言語っぽいシンタックスにしてみる。
switch {
case r < SALT_THRESHOLD:
0xffffffff
case (1.0 - r) < PEPPER_THRESHOLD:
0xff000000
default:
input(x, y)
}
野暮ったさはあるが見慣れているので読みやすくはあるな。
やはりあまりstatementっぽくはしたくない。kotlinやrustなどはstmt的にかけるがexpression、という感じだけれど、MFGの用途を考えるとstmtっぽいというのは嬉しくない。 それよりもexpressionぽさが強い方が良い。 でもシンタックスシュガーの入っている方が読みやすい。
関数呼び出しなのが分かりつつシンタックスシュガーを入れるような方法は無いものか?
関数呼び出しの欠点を考える。
def result |x, y| {
let r = rand()
ifel( r < SALT_THRESHOLD,
0xffffffff,
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
))
}
まず最初のifelの行が、開きっぱなしな感じがして行の末で終わった感じがしない。 それと最後の閉じカッコのネストがかっこ悪い。
カッコの中にシンタックスシュガーを入れてみる。
cond(
r < SALT_THRESHOLD:
0xffffffff
(1.0 - r) < PEPPER_THRESHOLD:
0xff000000
else:
input(x, y)
)
うーん、あんまり関数呼び出しっぽさは無いなぁ。
最後のlambdaはカッコの外に出せる、みたいなのと同じ感じで、最後の引数を外に出す仕組みがあれば良い気はする。 例えば...で終わっていると、その次の式を引数に入れる、とかはどうだろう?
def result |x, y| {
let r = rand()
ifel(r < SALT_THRESHOLD
0xffffffff, ...)
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
)
}
なんかこの方向がいい気がするな。
次の式の値を関数の最後の引数に取り込む、という機能を考える。 ...の代わりにパイプ記号とかはどうだろう?
def result |x, y| {
let r = rand()
ifel(r < SALT_THRESHOLD
0xffffffff, |>)
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
)
}
悪くない気がする。関数の外に出すのはどうだろう?
def result |x, y| {
let r = rand()
ifel(r < SALT_THRESHOLD
0xffffffff) |>
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
)
}
うーん、なんか違うな。上の方が良い気がする。
暫定結論。
- 関数でifelで行く
- 最後の引数が...の時は次の式の値をその引数とする
書いてみると、このシンタックスならelifが欲しくなるので、elifはifelの別名関数とする。
スプレッドは他の言語で違う意味で使われているのでもっと別のものは考えられないか?
ifel(r < SALT_THRESHOLD
0xffffffff, ...)
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
)
アンダースコアを混ぜたなにかにしたい気はする。
ifel(r < SALT_THRESHOLD
0xffffffff, _<)
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
)
なんか条件分岐とかぶって見えるな。大なりでも一緒か?
ifel(r < SALT_THRESHOLD
0xffffffff, _>)
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
)
一緒か。アンダースコア2つでどうだろう?
ifel(r < SALT_THRESHOLD
0xffffffff, __)
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
)
これは悪くない気がするが、印字されたものは一つと区別がつきにくい、という問題はあるな。初学者がドキュメントを読んで間違えやすいかもしれない。
コロンとかつけてみるか?
ifel(r < SALT_THRESHOLD
0xffffffff, _:)
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
)
いまいち。やっぱり...が一番しっくりくるな。スプレッドが必要になったらそっちに違うトークンを割り当てよう。たぶんスプレッドは必要にはならないし。
なぜスプレッドが必要になる事は無いと思っているかも簡単に書いておく。
普通の言語では関数定義というのが基本になるのだが、MFGではTensor定義がカーネルになるので、他の言語の関数とテンソル定義はかなり機能がかぶっている。 複数のテンソルに分ける事でコードが分割されるので、他の言語ほど関数定義というのが重要な要素にならないと思っている。 むしろカスタムのトランスフォームなどを実装したいと思う事の方が多いんじゃないか。
tensorが特別扱いされていて言語がimmutableなので、汎用の関数はそれほど出番が無いと思っている。 そもそもにカーネルの計算量が増えると実機では落とされてしまうので、一つのカーネルをあまり複雑にはできない。 複雑な事をするにはどうにか複数のカーネルに分ける必要がある。だからカーネルの中をさらに構造化するのはそれほど重要では無いと思っている。
アンダーバーとパイプ記号はどうだろう?
ifel(r < SALT_THRESHOLD
0xffffffff, _|)
ifel( (1.0 - r) < PEPPER_THRESHOLD,
0xff000000,
input(x, y)
)
なんか違うな。やはり...にしよう。