Help us understand the problem. What is going on with this article?

コンマ演算子を駆逐するイディオムを思いついた

More than 3 years have passed since last update.

コンマ演算子による順次評価

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) に渡したテンプレート引数 funrange の要素を引数としてとる関数です。今回の例でその引数として渡されるのは () => 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, ..., exprNstd.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 コマンドを置いておきますね。バグってたらごめんなさい。

e10s
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away