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, ...)
と記述することはできません。
// これが
writeln("hello,world!");
// こうなる
"hello,world!".writeln;
何がうれしいのか
疑似的なメンバ関数により、ユーザー定義型をシンプルに記述できてうれしい!
UFCSは、オブジェクトの疑似的なメンバ関数のような見た目なので
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言語の仕様を組み合わせると
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問目を解いてみた」
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できない
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.functional
のpipe
やcompose
でつなげる方法もあるそうです
- 多段階にはUFCSできない (当たり前)
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
}