コンマ演算子による順次評価
expr1, expr2, ..., exprN
のようなコンマ演算子を使った式(引数リストじゃないよ)は左から右に、すなわち expr1
から順に評価され、exprN
を評価した結果が式全体の評価結果となります。イディオム的に使われますが、邪悪です。
その邪悪さゆえにこのような演算子を用意していない言語もあるらしく、D 言語でも言語仕様から抹殺しようという動きがあったこともあります。
D 言語教の 1 宗派
唐突ですが。
自分も好きです。UFCS チェインで書くのが生きがいです。また、import
宣言以外にセミコロンを複数回使ったら負けかなと思っているワンライナー派です。
ダサいワンライナー
普通の例
こういう処理があったとします。
auto add1(int n)
{
return n + 1;
}
import std.stdio;
void main()
{
int n = -1;
writeln(++n);
n = 3;
writeln(n);
n *= 2;
writeln(n);
auto r = add1(n + 999);
}
次のような出力結果が期待されます。
0
3
6
main
関数にはセミコロンが 7 つもあるので大敗です。
コンマ演算子の例
main
関数の中は単純な式を順々に評価していくだけなので、コンマ演算子を使って次のように書けます。
int n = -1;
auto r = (writeln(++n), n = 3, writeln(n), n *= 2, writeln(n), add1(n + 999));
セミコロンが 2 つに減りました。とてもダサい上に、負けです。さらにラムダを使って次のように書きます。
auto r = (n => (writeln(++n), n = 3, writeln(n), n *= 2, writeln(n), add1(n + 999)))(-1);
ラムダに引数として -1
を直接与え、即時に呼び出しています。変数宣言を消したいときによく使っています。これ自体は今回の目的からは脱線しますが。セミコロンは 1 つだけですが、まだダサいです。
コンマ演算子は大変よいものなのですが、優先順位が低いので式全体を括弧でくくる必要があり、見た目も引数リストみたいでなんか許せません。ただセミコロンをコンマに置き換えただけの、ワンライナーの風上にも置けない代物として扱います。
Effective T
std.range.tee
std.range.tee
というレンジ処理用の関数があります。以前、この tee
の解説を書いたので見てほしいのですが、ちょっとトリッキーな関数です。
tee
がコンマ演算子を駆逐する
これこそが今回紹介したいイディオムです。
import std.range;
int n = -1;
auto r = [() => add1(n + 999)] // 最終的な評価値を返すラムダのみからなる
.tee!(_ => writeln(++n), No.pipeOnPop)
.tee!(_ => n = 3, No.pipeOnPop)
.tee!(_ => writeln(n), No.pipeOnPop)
.tee!(_ => n *= 2, No.pipeOnPop)
.tee!(_ => writeln(n), No.pipeOnPop)
.front()(); // front はラムダを返すプロパティ(関数)
何なんだこれは。tee
だらけの UFCS チェインですよ。もちろん、コンマ演算子バージョンのように書き直せば大勝利です。
auto r = (n =>
[() => add1(n + 999)]
.tee!(_ => writeln(++n), No.pipeOnPop)
.tee!(_ => n = 3, No.pipeOnPop)
.tee!(_ => writeln(n), No.pipeOnPop)
.tee!(_ => n *= 2, No.pipeOnPop)
.tee!(_ => writeln(n), No.pipeOnPop)
.front()()
)(-1);
意図的に改行していますが、ワンライナーと呼べる具合になりました。宗教上の理由でまた脱線しました。
回したいレンジは [() => add1(n + 999)]
で、これは最終的に評価したい式 add1(n + 999)
を評価して返す引数なしラムダのみからなる配列です。
front()()
?
ここにおけるすべての tee
は [() => add1(n + 999)]
というレンジを受け取り、そのレンジをラップしたレンジを返します。それゆえ、その返されるレンジの front
プロパティは、ラムダ () => add1(n + 999)
を返します。
実のところ front
プロパティの正体は関数なので、括弧付きで呼び出しても同じことです(@propety
付きなのでプロパティ記法を使って括弧を省略して呼び出すべきですが)。その戻り値である引数なしラムダを呼び出す((() => add1(n + 999))()
相当のことをする)ために、front()()
という妙なことになっています。この辺はコンパイラの挙動とのすりあわせなので本質的なところではないのですが、ちょっと注意が必要です。
ただ、最後に評価したい式(add1(n + 999)
の部分)は定数、たとえば constN
だという場合は、[() => constN]
ではなく [constN]
のようにできるので、そのときは ()()
は不要です。
tee!(fun, No.pipeOnPop)(range)
?
tee!(fun, No.pipeOnPop)(range)
に渡したテンプレート引数 fun
は range
の要素を引数としてとる関数です。今回の例でその引数として渡されるのは () => add1(n + 999)
ですが、ここにおいてはダミー扱いです。アンタッチャブルな産廃なので呼び出さないでください。。
では tee
に与えたラムダはいつ呼ばれるのかというのが気になります。実は先述の解説記事で検証しているわけですが、テンプレート引数の No.pipeOnPop
が肝です。
実際、これらの tee
に与えたラムダは(この書き方では)上から順に呼ばれます。そして最後に () => add1(n + 999)
が呼び出され、全体として目的の処理が達成されるのです。
注意として、No.pipeOnPop
を忘れるとデフォルト引数の Yes.pipeOnPop
が使用され、結果としてその tee
に与えたラムダは呼び出されません。
注意点を踏まえたバリエーション
[true]
.tee!(_ => hoge(), No.pipeOnPop)
.tee!(_ => assert(0)) // Yes.pipeOnPop な tee なので到達しない
.tee!(_ => fuga(), No.pipeOnPop)
.front; // ()() 不要
コンマ演算子を駆逐しつつも順次評価するイディオム
以上のことを一般化すると、コンマ演算子を使った式 expr1, expr2, ..., exprN
は std.range.tee
を濫用することで(あらゆる場面でかはわからないものの)置換可能です。
import std.range;
[() => exprN]
.tee!(_ => expr1, No.pipeOnPop)
.tee!(_ => expr2, No.pipeOnPop)
................
.front()()
コンマな式の最右辺が定数 constN
であり、expr1, expr2, ..., constN
のような形であれば、構文糖衣が使えます。
[constN]
.tee!(_ => expr1, No.pipeOnPop)
.tee!(_ => expr2, No.pipeOnPop)
................
.front
感想
そういうテンプレート関数を std.functional
あたりにぶち込むのが得策だと思います。
謝辞
このハックは ueshita 先生謹製の dl コマンドのワンライナー化ハックをしているときに思いつきました。ありがとうございます。
せっかくなので、tee
を躍動させることで高度なレンジ化に成功した dl コマンドを置いておきますね。バグってたらごめんなさい。