本日はコンパイラ未来話ということで、現状まだ未実装な、あるいは意味論がはっきりしていないものについて書いていきたいと思います。
従って、今回は後半になるほど渋い話ばかりになります…
UFCSの制限緩和
ローカルimportと組み合わせ可能にすることがまず必要だと思っています。
void main() {
import std.algorithm;
auto r = [1,2,3].map!(x => x * 2);
assert(equal(r, [2,4,6]));
}
現状見えているコーナーケースとしては、
class C {
static auto foo(R)(R r) { return r.map!(x => x * 2); }
auto hoo(R)(R r) { return r.map!(x => x * 2); }
void test() {
// 1. staticメンバ関数はUFCSで呼べるべき?
[1,2,3].foo();
// 2. 非staticなメンバ関数はUFCSで呼べるべき?
[1,2,3].hoo();
// 3. 関数内関数はUFCSで呼べるべき?
auto bar(R)(R r) { return r.map!(x => x * 2); }
[1,2,3].bar();
// 4. bazはaかbのどちらにヒットする?
[1,2,3].baz();
}
void baz() {} // a
}
R baz(R)(R) { return r.map!(x => x * 2); } // b
今の所、 1. Yes, 2. No, 3. No, 4. bにヒット として実装することを考えています。詳細な理由としては
- UFCSは暗黙のコンテキストを持たない関数がヒットするべき、よってstatic関数はUFCSで呼び出せる
- 1.と同じ理由で、メンバ関数は暗黙のthisを持つのでUFCSではヒットするべきではない
- 2.と同じ理由
- 2.の理由からメンバ関数のbazにはヒットせず、結果bにヒットする
といった感じです。これらは通常の名前探索ルールと異なる挙動なので悩ましいのですが、利便性を考えると妥当かなと考えてます。
テンプレート関数と非テンプレート関数のオーバーロード
簡単なworkaroundが存在するのでこれまで自分の中では優先度を下げていましたが、そろそろ取り掛かりたいとは思ってます。
isolated(unique) 特性
pure関数に於いて、戻り値の型がパラメータに出現しない場合、戻り値は常にそのpure関数内で新しく構築された値とみなすことができます。
pure int[] makearr(int n);
auto arr1 = makearr(1);
// arrは常に新しくヒープに確保された値であることが保証できる
pure int[] makedup(const(int[]) org);
auto arr2 = makedup(arr1);
// const(int[]) は int[] に暗黙変換できないので、同じく戻り値が
// 新しく確保された値であることを保証できる
つまりこういった値は、たとえ型がmutableであってもimmutableな値に暗黙変換できる、という特性を持っていると考えることができます。
immutable int[] arr3 = makedup(arr2);
// makedupから返されたmutableな値は外部の参照に
// 捕捉されていないことが保障されるなので、
// ここでのimmutableへのキャストは型安全になる
このような特性を暫定的にisolatedまたはunique特性と呼んでいますが、これを正しくコンパイラで捕捉してやればオブジェクトの構築周りで安全でないキャストを減らせる、と考えています。
現状はstrong purityな関数との組み合わせである程度サポートされており、これを利用したdup
の実装を提案しています。
https://github.com/D-Programming-Language/druntime/pull/298
property enforcement(@propertyの強制)
どのようにふるまうべきか、という点でいくつか問題がまだ残ってます
-
prop(arg1, arg2)
という関数呼び出しがあった場合、prop
がプロパティ関数の場合はprop()(args1, arg2)
になってほしい実装は難しくないのですが、-propertyスイッチの有無でコードの意味論が変わってしまうという問題があります。
これについては破壊的変更が不可避なので、何らかの合意をコミュニティ内で得る必要があります… -
r = arr.map!(x => x)();
で、余計な()
を省きたい@propertyが付かない通常の関数を呼び出す際、括弧の省略を禁止したいという意見があるのですが、rangeやalgorithmの関数群をUFCSで呼び出す際は上記のように一見無駄に見える括弧が必要になってしまいます。これについては
- mapは「プロパティではない」のだから、@propertyはつけないし、余計に見える括弧も常に書くべき
- 余計な括弧は余計だからつけたくない
- mapに@propertyを付けよう
- UFCSの場合は括弧の省略を許容しよう
などの意見がforumで出ており、結論もまだ出ていません。
自分の中では「通常の関数でも、引数なしで呼ぶ場合は括弧を省略できる」特別ルールを導入するのが一番妥当かなと思ってます。 -
関数ポインタ・delegate変数をプロパティとして扱えるようにするか
以下の様に、@propertyが付いた型を持つ関数ポインタはプロパティとして扱えるべきという意見が出ています。
alias PFGet = int function() @property; alias PFSet = void function(int) @property; PFGet pget = ...; int x = pget; PFSet pset = ...: pset = 1;
…が、自分はこれは実装すべきではないと思っています。pgetやpsetをプロパティとして使えるということは、それらを関数ポインタそのものとして使えない、ということと同義ですから、コードを書きづらくなる負の側面のほうが大きいと思ってます。(今思いつきましたが、genericなコードで@propertyに対する考慮が必要になってしまう、という点でもよくないですね…)
-
アドレス演算子の問題
D言語はC/C++とは異なり、関数から関数ポインタ(またはdelegate)を得る際には常にアドレス演算子が必要です。しかしここにプロパティを組み合わせると、以下の様な例でどうするべきかという問題があります。
int g; @property ref int foo() { return g; } void main() { auto x = &foo; // todo }
これまで
&foo
は常にfooの関数ポインタを返していました。しかしfooはプロパティでもあるのでxは「fooの返す値のアドレスとなるべき」という主張も出ています。この主張自体はそれなりに理があるのですが、これを認めると- 既存コードの意味が突然変わる
- fooの関数ポインタを取る方法が言語からなくなってしまう
という点があり、特に自分としては後者が大問題だと考えています。
関数シンボルそのものは実行時の値にはなれませんが、関数ポインタは実行時の値です。前者から後者への変換を行える(今のところ)唯一の言語機能がアドレス演算子であり、これが使えなくなるのは言語の機能を削除するのと同じことになってしまいます。また関数型を扱うメタプログラミングもこの機能に依存しているため(たとえばstd.traits.FunctioinTypeOf)、そちらの観点からも問題が大きいと考えています。
上記の理由から挙動変更するなら__traitsなどで既存機能の代替を追加する必要があり、それなら変更しない方がましなのでは、と思っています。
ただ「プロパティ関数の戻り値のアドレスを取る」目的なら、上のような挙動変更を入れなくても、補助関数を1つ用意すれば問題なく行けます
// 左辺値を取り、左辺値を返す非@property関数 ref identity()(ref T arg) { return arg; } int g; @property ref int foo() { return g; } void main() { // fooは関数の引数として先にプロパティ呼出し解決されるので、 // `x == &g`が常に成り立つ auto x = &identity(foo); // UFCSを使うならこういう書き方もできる auto x2 = &foo.identity() }
このような変更を必要とするケースには、実例として
std.range.moveFront
がありますが、アドレス演算子の挙動変更よりはましなやり方だと思っています。
IFTIにおけるunification
以下の様なコードは現状コンパイルできません。
void foo(T)(T t, T delegate(T) dg) {}
void main() {
foo(1, x => x * 2);
}
現在IFTIには、各関数引数間で推論動作が独立していなくてはならないという制限があります。
上記の例では、fooの型パラメータT
を推論する際、第一引数tからの推論結果(T←int)が、第二引数dgに与えられたlambdaの型推論に影響しているために上記の制限を破っており、従ってコンパイルに失敗するわけです。
これが動いて欲しいという要求はforumでも何度か出ているのですが、実際にこれをやろうとすると型パラメータと実引数の型の間で単一化(unification)が必要になり、型推論機構に大幅な手を入れなくてはならなくなるため、メリットと実装コストの兼ね合いから私の中の優先度はかなり低くなってます…
postblitの意味論
postblitは効率的なビットコピーと論理コピーを組み合わせた機能ですが、実は意味論に問題を抱えています。
- 「mutableなオブジェクトからconstなコピーを作る」などの修飾が強くなる方向
- 「constなオブジェクトからconstなコピーを作る」などの修飾が変わらない方向
の2種類は、コンパイラの実装が追い付いていないだけで意味論的には問題ないのですが、
- 「immutableからmutableなコピーを作る」などの修飾が弱くなる方向
では、postblitの意味論ではうまく対応できないことが見えています。
例:immutable→mutableの例では、postblitの内部で型システムがconst性を保証できない
void main() {
immutable S si = immutable S([1,2,3]);
S sm = si; // copy
}
struct S {
int[] arr;
this(this) {
// siからsmのコピーでは、この時点でarrはimmutable(int[])を指している
pragma(msg, typeof(this.arr));
// しかしarrはint[]として型付けされてしまっている!
}
}
Andreiはこの様なケースでは、postblitではなくコピーコンストラクタが必要だと考えているようです。
TDLPでもそのような記述がなされています。
私もこの問題を認識してからいろいろ考えてみましたが、コピー=postblitの方程式を崩すしかなさそうです…
auto refと効率的な引数の渡し方
auto ref
は左辺値と右辺値の両方を受け取れる関数引数を持つために使用可能ですが、いくつかの既知の問題があります
- テンプレート関数でしか使えない
- 左辺値を受けたときと右辺値を受けたときで別々のテンプレートを実体化するため、code bloatingが起きやすい
(n個のパラメータをauto ref
で宣言すると、最悪2^n個の実体化が起きる)
また、D言語には今の所、右辺値を(C++と異なり)参照で受け取ることができません。これは(move semanticsの観点などから)意味論の単純化に貢献していますが、実行時の効率という点では決して良いとは言えません。
// 引数Tは必ずムーブ(ビットコピー)またはコピーされる
// Tのサイズが大きい and fooが何度も呼ばれる場合、性能問題になりやすい
void foo(T t) {}
この点についてのforumの議論は、今の所対策の必要性は認識されているものの、具体的なsyntaxの点でいい結論が出てない状況です。
クラスオブジェクトでのconst性
現状、object.Objectが持つ4つのメンバ関数はconstとしてマークされていません。
string toString();
nothrow @trusted hash_t toHash();
int opCmp(Object o);
bool opEquals(Object o);
これは事実上、constなクラスオブジェクトに対してこれら4つが型システムの保証付きで動かない、ということでもあります。(現状動いているように見えるのはdruntimeにhackyなコードが入っているため。このためconstなクラスオブジェクト同士の等値比較は現状const性が壊れてしまっている)
2.060の開発で一時期これらにconstを付けることが行われたのですが、constの強制は制限が強すぎる、既存コードを壊す、などの理由からrevertされました。
問題の難しさからか、時々上がるforumの議論もあんまり進まずにフェードアウトすることが多いです…
最近では、UFCSを使ってこれらのメンバ関数を外付けする案も出たようです(オーバーライドとかはどうするんだと思いましたが)。
現時点での大きい未解決問題を挙げてみましたが、皆さんはどう思われたでしょうか。
私が直接関わっていることのみ挙げたので、ここに書かれていないまた別の問題もありますが、私としては「今後の発展の余地があるんだなあ」と好意的に見ていただけたら幸いです。
明日20日は@psihyさんです。