Edited at

D言語のUFCSが好きだ!

More than 3 years have passed since last update.


D言語の特徴的な機能としてUFCSがある

他の言語にも同様の機能があるようですが、D言語のそれには使っていて楽しいと思えるいくつかの特徴があり、好んで使うD言語erも多いようです。(私もよく使ってます!)

私がUFCSするときに感じた、利点などをまとめてみました。

[dmd2.068.2にて動作確認]


UFCSとは

UFCS (Unified Function Call Syntax)とは、関数呼び出しについてのシンタックスシュガーで、ほとんどの関数について、func(obj, ...)obj.func(...)と記述することを許すというシンプルな機能です。

挙動としては、obj.func(...)が存在しない場合、func(obj, ...)の呼び出しを試みるという感じです。

※逆に、obj.func(...)func(obj, ...)と記述することはできません。


D

// これが

writeln("hello,world!");

// こうなる
"hello,world!".writeln;



何がうれしいのか


疑似的なメンバ関数により、ユーザー定義型をシンプルに記述できてうれしい!

UFCSは、オブジェクトの疑似的なメンバ関数のような見た目なので


D

import std.stdio;

/**
* 大量のヘルパー的なメンバ関数で、クラスの構造がわかりづらくなる
* また、クラスの定義とヘルパー的なメンバ関数の間に、依存関係ができてしまう
* かといってヘルパー関数としてクラス外に出すと、関数の呼び出しが使いづらい
*/

class C1
{
// 型定義がごちゃごちゃ
private int n_;
this() { }
void helper1() { "あんなこと".writeln; }
void helper2() { "こんなこと".writeln; }
void set(int n) { n_ = n; }
void helper3() { "そんなこと".writeln; }
int get() { return n_; }
void helper4() { "どんなこと".writeln; }
}
// C1型に依存
void helper5(C1 x) { "わいわい".writeln; }
void helper6(C1 x) { "がやがや".writeln; }

/// そこでUFCS!!
class C2
{
// 型定義はシンプルに
private int n_;
this() { }
void set(int n) { n_ = n; }
int get() { return n_; }
}
// 依存性は抑えめに
void helper1(T)(T x) if (is(T : C2)) { "あんなこと".writeln; }
void helper2(T)(T x) if (is(T : C2)) { "こんなこと".writeln; }
void helper3(T)(T x) if (is(T : C2)) { "そんなこと".writeln; }
void helper4(T)(T x) if (is(T : C2)) { "どんなこと".writeln; }
void helper5(T)(T x) if (is(T : C2)) { "わいわい".writeln; }
void helper6(T)(T x) if (is(T : C2)) { "がやがや".writeln; }

void main()
{
// 通常
auto c1 = new C1;
c1.set(42);
writeln(c1.get);
c1.helper1; // 型定義にこれ必要?
c1.helper2; //  〃
c1.helper3; //  〃
c1.helper4; //  〃
helper5(c1); // 仲間外れでつらい
helper6(c1); //  〃

// UFCS!!
auto c2 = new C2;
c2.set(42);
c2.get.writeln;
c2.helper1; // まるでメンバ関数!!
c2.helper2; // 〃
c2.helper3; // 〃
c2.helper4; // 〃
c2.helper5; // 〃
c2.helper6; // 〃
}


型定義の本体はシンプルな内容に留め、かつ疑似的なメンバ関数として同様の処理を提供できます!

(おそらくこれが本来のUFCSの目的?)


関数をどんどんつなげて記述できるチェイン記法がうれしい!

UFCSと、引数のない関数は、関数呼び出しの()を省略できるというD言語の仕様を組み合わせると


D

int hoge(int n) { return n + 2; }

int fuga(int n) { return n * 3; }
int piyo(int n) { return n / 5; }

void main()
{
// 変数xの値をhogeした後、fugaして、piyoした結果を表示したい
auto x = 68;

// 通常の記述
import std.stdio;
writeln(piyo(fuga(hoge(x))));

// ↓
writeln(piyo(fuga(x.hoge)));
// ↓
writeln(piyo(x.hoge.fuga));
// ↓
writeln(x.hoge.fuga.piyo);
// UFCS !!
x.hoge.fuga.piyo.writeln;
}


こんな感じで、UFCSチェインして気持ちよく記述できます!

(これがしたくて使っていると思う!)


その他の効能

TODO: 適当なコード片をサンプルとして書き足す

[2015.10.17サンプル追記]


  • ある程度複雑な処理をワンライナーで記述できる!


  • ネストされた()が減ることで、入力が楽で間違えにくい!


  • 視線の方向(左から右)に記述でき、処理の流れが追いやすい!

元ネタ:「1時間以内に解けなければプログラマ失格となってしまう5つの問題が話題に

参考にしました→「Java8で「ソフトウェアエンジニアならば1時間以内に解けなければいけない5つの問題」の5問目を解いてみた


D

import std.range     : iota, array;

import std.conv : to;
import std.algorithm : map, filter, sum, each;
import std.array : join, split;
import std.stdio : writeln;

R combination(R, R sep = [" +", " -", ""])(R elem)
{
return elem.length < 2
? elem
: sep
.map!(op => combination(elem[1 .. $])
.map!(x => elem[0] ~ op ~ x))
.join;
}

void main()
{
iota(1, 10).array
.to!(string[])
.combination
.filter!(a => a.split(" ").to!(int[]).sum == 100)
.each!(a => a.writeln);
}



  • 関数設計時の引数順序のゆるい指針: とりあえずUFCSでつながるように!


はまり所


  • メンバ関数、ネスト関数、ラムダ式はUFCSできない


D

class C

{
// メンバ関数
static int sfoo(int n) { return n * 2; }
int nfoo(int n) { return n * 2; }
}

void main()
{
// ネスト関数
static int sbar(int n) { return n * 2; }
int nbar(int n) { return n * 2; }
// ラムダ式
static auto sbaz = (int n) => n * 2;
auto nbaz = (int n) => n * 2;

import std.stdio;
//1.C.sfoo.writeln; // NG
//1.(new C).nfoo.writeln; // NG
//1.sbar.writeln; // NG
//1.nbar.writeln; // NG
//1.sbaz.writeln; // NG
//1.nbaz.writeln; // NG
}


[2015.10.17追記]

→UFCSできない場合、std.functionalpipecomposeでつなげる方法もあるそうです


  • 多段階にはUFCSできない (当たり前)


D

int foo(int a, int b, int c) { return a + b + c; }

void main()
{
import std.stdio;
auto a = 1, b = 2, c = 3;
a.foo(b, c).writeln; // OK
//b.a.foo(c).writeln; // NG
//c.b.a.foo.writeln; // NG
}