C#のgenericsでは、型パラメータを持つ型パラメータを制約するには余分な型パラメータを増やすしかありません(私が知らないだけかもしれないです)。
これが非常に不便で、genericsを駆使して抽象化を行ってゆくと、この制約に泣かされることになります。
何のことか分からないですね。とりあえず、次のプログラムを見てください。

例えば、次のプログラムでは、クラスFooに2つの型パラメータにCarとPersonを代入して変数tFooを宣言してます。
第2型パラメータのPersonを指定することが、情報量的に無駄です。
なぜならば、第1型パラメータ「Car;」とクラスFooの1つ目のwhere句の右辺「Car<_Owner>;」をユニファイ(パターンマッチ)すれば、_OwnerにPersonが代入されていることは推論できるからです。

namespace MyApplication 
{ 
	public interface Person 
	{
		string Name { get; }
	} 
	public interface Car<_Owner> 
	where _Owner : Person 
	{
		string Name { get; }
		_Owner Owner { get; }
	}
	public interface Foo<_Car, _Owner> 
	where _Car : Car<_Owner> 
	where _Owner : Person { } 
	public class Program
	{
		public static void Main() 
		{
			Foo, Person> tFoo = ...; 
		} 
	}
}

ローカル型変数の定義とそれを使ったユニフィケーションをC#のgenericsに導入するだけで実現できるのに、このような機能がないなんて不思議です。
もし、このような機能があれば上記のプログラムは次のようになります。変数宣言が簡潔になり、うれしいです。

namespace MyApplication 
{ 
	public interface Person 
	{ 
		string Name { get; }
	}
	public interface Car<_Owner> 
	where _Owner : Person 
	{
		string Name { get; }
		_Owner Owner { get; }
	} 
	public interface Foo<_Car> local _Owner 
	where _Car : Car<_Owner> 
	where _Owner : Person { } 
	public class Program 
	{ 
		public static void Main() 
		{ 
			Foo> tFoo = ...; 
		}
	}	
}

ユニファイによる推論がない言語では、型パラメータの代入によって作れらる木(Foo;, Person>;など)の部分木(Car;やPersonなど)を制約しようとすると、部分木を指定するための型パラメータを増やさなければなりません。
genericsを駆使して様々な型を交換可能にしたライブラリを作ろうとすると、このような問題に悩まされます。
そもそも、交換可能な部分が多いとC#のgenericsでは型パラメータが増えまくって大変です。そういうことをやるなら、gbetaなどのファミリーポルモルフィズムが使える言語がいいですね。

カテゴリー: 技術情報

11件のコメント

pon · 2011-08-03 20:40

はじめまして。

実現したいことは、以下のコードとは違うのでしょうか?
(インターフェイス Foo の定義を変えました)
———————————————————————————————————————-
namespace MyApplication
{
public interface Person
{
string Name { get; }
}
public interface Car
where _Owner : Person
{
string Name { get; }
_Owner Owner { get; }
}
public interface Foo
where _Owner : Person
{
string Name { get; }
_Owner Owner { get; }
Car Car { get; }
}
public class Program
{
public static void Main()
{
Foo tFoo = …;
}
}
}

pon · 2011-08-03 20:44

すみません、先ほどの投稿がうまくできなかったようです。
要するに、ジェネリックインターフェイスFooの型パラメータを Owner ( where Owner : Person ) のみにするのではいけないのか、
ということを言いたかったのです。

mel · 2011-08-09 03:23

ponさん、コメントありがとうございます。返事が遅くなりましたが、ご容赦を。

Foo にメソッドが定義されていないのが、説明する上で適切ではありませんでした。
引数または戻り値の型が _Owner と _Car に依存するメソッドを Foo が持つ場合はどうでしょう?
例えば、所有者が持っている車の中で最も良い車を返すメソッド「_Car getBestMyCar(_Owner aOwner)」を宣言しようとすると、私が困っている状況に遭遇します。

Foo の型パラメータを _Owner のみにすることは、型パラメータ _Car を特定の型に束縛することを意味しますが、私が困っている状況では、まだ _Car を束縛したくありません。_Car を束縛して「Car<_owner> getBestMyCar(_Owner aOwner)」というメソッドにした場合、このメソッドの戻り値として受け取った車を F1Car (特定の Car のサブクラス) として扱いたい場合、キャストが必要になるからです。

Foo を含むライブラリの利用者が getBestMyCar の戻り値の型を決めることに価値がある(キャストを減らすことに大きな意味がある)場合には、ライブラリの作者は _Car を束縛しないでしょう。その場合に、私が困っている状況に遭遇します。

pon · 2011-08-23 21:05

返事ありがとうございます。

つまり、Foo 型は 2 つの型 TCar, TOwner に依存して決まる型であり、
制約 where TOwner : Person, where TCar : Car<TOwner>を満たす任意の TCar, TOwner について定義可能にしたいということでしょうか。

そうであれば、Foo 型の定義は mel さんの最初の例の通り、
2 つの型パラメータをもつ Foo<TCar, TOwner>として(制約を指定して)定義するのがセオリーだと思います。

ただ、その場合
> 第2型パラメータのPersonを指定することが、情報量的に無駄です。
> なぜならば、第1型パラメータ「Car;」とクラスFooの1つ目のwhere句の右辺「Car;」をユニファイ(パターンマッチ)すれば、_OwnerにPersonが代入されていることは推論できるからです。
ここの理屈が分かりません。
Foo<Car<Person>, TPerson> が整合的に定義可能な型 TPerson が Person 以外にありえないと、どのような理屈で結論づけられるとお考えなのでしょうか。
(制約条件などから、コンパイラがこの結論を下すことがC#の仕様的に可能なのでしょうか?)

なお、上記から少し話が変わりますが・・・
Foo<TCar, TOwner>を TCar = Car の関係で利用することが多く、
その場合に型指定の(2つの型TCar, TOwnerを指定しなければならない)単調さを解消したいということであれば、
型パラメータ TOwner (where TOwner : Person) を 1 つだけもつジェネリック型 Foo2<TOwner> を
Foo<Car<TOwner>, TOwner> の継承型として定義し、その Foo2<TOwner>を利用する手法が考えられます。

pon · 2011-08-23 21:09

すみません、上記の後半5行がうまく投稿できなかったようなので、その部分だけ再投稿させていただきます。

なお、上記から少し話が変わりますが・・・
Foo<TCar, TOwner>を TCar = Car<TOwner> の関係で利用することが多く、
その場合に型指定の(2つの型TCar, TOwnerを指定しなければならない)単調さを解消したいということであれば、
型パラメータ TOwner (where TOwner : Person) を 1 つだけもつジェネリック型 Foo2<TOwner> を
Foo<Car<TOwner>, TOwner> の継承型として定義し、その Foo2<TOwner>を利用する手法が考えられます。

pon · 2011-08-23 21:18

上記の Foo2 についての補足です。

Foo<Car<TOwner>, TOwner> の継承型として定義する Foo2<TOwner> の名前は、
Foo<TOwner> としてもOKです。
関数のオーバーロードと同じで、名前が同じだが型パラメータの個数が違うジェネリック型の定義は別々のものとして認識され、
競合しない(共存できる)ためです。

mel · 2011-08-26 16:38

> > 第2型パラメータのPersonを指定することが、情報量的に無駄です。
> > なぜならば、第1型パラメータ「Car;」とクラスFooの1つ目のwhere句の右辺「Car;」をユニファイ(パターンマッチ)すれば、_OwnerにPersonが代入されていることは推論できるからです。
> ここの理屈が分かりません。
> Foo<Car<Person>, TPerson> が整合的に定義可能な型 TPerson が Person 以外にありえないと、どのような理屈で結論づけられるとお考えなのでしょうか。
> (制約条件などから、コンパイラがこの結論を下すことがC#の仕様的に可能なのでしょうか?)
私の理屈は以下の通りです。

Foo<_Car, _Owner> を具体化するために2つ目の型パラメータを指定しなくてもよいことを示します(現在の C# の仕様では指定しなくてはなりません)。具体的には、Foo<Car<Teacher>, x> を計算して2つ目の型パラメータ x に何が代入されいるかを調べます。ただし、Teacher は Person のサブクラスであるとします。さらに、クラス A がクラス B のサブクラスであることを「A ≦ B」と表すことにします。

Foo<_Car, _Owner> の定義より
 _Car ≦ Car<_Owner> … (A)
 _Owner ≦ Person … (B)

Foo<Car<Teacher>, x> より
 _Car = Car<Teacher> … (C)
 _Owner = x … (D)

(A) と (C) より
 Car<Teacher> ≦ Car<_Owner>
Car の型パラメータは invariant なので (※)
 Teacher = _Owner … (E)

(D) と (E) より
 x = Teacher

以上

(※) C#4.0 以前では invariant な型パラメータしか宣言できず、C#4.0 以降では型パラメータを宣言するときに in または out を指定しないと invariant な型パラメータを宣言したことになります。

この証明は「サブクラス関係を考慮したユニフィケーション」を計算しなければならないのでややこしいですが、_Owner が invariant な型パラメータであることが分かっているので、単なるユニフィケーションを計算するだけで解けます。

この証明で「x = Teacher」を求めることができました。つまり、Foo<Car<Teacher>, x>の x が Teacher であることは自動的に決まるのでプログラマが指示しなくてもよいはずです。しかし、今の C# の言語仕様では指示しなくてはいけません (最近、C# を触っていないので状況が変わっているかもしれません)。

私は x のように自動的に決まるものを指定しなければならない言語仕様が恰好悪いと考えています。これを恰好よくするには、自動的に決まるものを指定しない言語仕様を作ればいいだけですが、自動的に決まるものかどうかをパッと判断するのは難しいのでキーワード「local」などを用意して明示させるのがよい気がします。

pon · 2011-08-26 21:53

回答ありがとうございます。
理屈について納得しました。

> 私は x のように自動的に決まるものを指定しなければならない言語仕様が恰好悪いと考えています。
> これを恰好よくするには、自動的に決まるものを指定しない言語仕様を作ればいいだけですが、自動的に決まるものかどうかをパッと判断するのは難しいので
> キーワード「local」などを用意して明示させるのがよい気がします。

他の情報から補完できる型情報をコンパイラが自動的に決定してくれると便利、というのは私も同感です。
ただし、「型情報をコンパイラが自動的に決定してくれる」ことは、
「型情報をコンパイラが、プログラマの意図とは違う結果として自動的に決定してしまう
 (そして、そのことにプログラマが気づきにくい場合さえある)」危険を孕んでいることも意味します。
また、下手な推論アルゴリズムだと、型推論処理に時間がかかり、開発環境として非常に使い勝手が悪いことになってしまいます。
ですので、型推論については、推論の妥当性・推論処理の効率という面で一定の水準に達していない推論アルゴリズムであれば、
(たとえそれにより便利になる場合があるとしても)導入してほしくないと思っています。
(C#3.0で導入された var は、その適用範囲だけでなく、推論の妥当性・推論処理の効率という面でみても申し分のない、大変素晴らしいものだと思っています。)

さて、お話にあったキーワード「local」による型推論について、もう少し詳しく教えてください。

(1)
例えば
 Bar<T1> local T2 where T1 : T2
と定義して、T1だけ型指定しT2の指定を省略する形で Bar を使った場合、
(T1の指定が System.Object でなければ)T2の型は一意に決まりません。
このとき、コンパイラはコンパイルエラーを発生させるべきでしょうか?

(2)
例えば、mel さんの 2 番目の例のジェネリック型定義
 Car<TOwner> ( where TOwner : Person )
と同様に、ジェネリック型
 Country<TOwner> ( where TOwner : Person )
が定義されていて、さらに 3 つの型パラメータ TCar, TCountry, TOwner をもつジェネリック型
 Foo<TCar, TCountry> local TOwner ( where TCar : Car<TOwner>, TCountry : Country<TOwner> )
が定義されているものとします。
Person 型の継承型 Person1, Person2 があり、プログラマが
 Foo<Car<Person1>, Country<Person2>> tFoo = …;
と書いた場合、コンパイラはコンパイルエラーを発生させるべきでしょうか?

(3)
例えば、mel さんの 2 番目の例のジェネリック型定義 Foo<TCar> local TOwner
とともに、型パラメータ TCar をもつ(従来の)ジェネリック型 Foo<TCar> が定義されていたとして、
プログラマが
 Foo<Car<Person>> tFoo = …;
と書いた場合、どちらのジェネリック型が適用されたと判断されるべきなのでしょうか?
(または、コンパイルエラーになるべきなのでしょうか?)

(4)
(1)~(3)の場合にも望ましい結果となるような、キーワード「local」による型推論のアルゴリズムとしてどのようなものが考えられるでしょうか?

mel · 2011-08-29 14:16

> さて、お話にあったキーワード「local」による型推論について、もう少し詳しく教えてください。
キーワード「local」に関して、次のような構文を導入すると分かりやすいと考えています。この構文では、パラメータ型変数宣言を並べた後に、ローカル型変数宣言を並べます。クラスを具体化する際に、パラメータ型変数には具体的な型が明示的に代入されますが、ローカル型変数には代入されません。代わりにローカル型変数に格納される型は型推論によって求まります。制約が少ないと具体的な型ではなく範囲を持った抽象的な型として求まることがあります。

class クラス名
  <パラメータ型変数宣言1, ..., パラメータ型変数宣言P>
 <ローカル型変数宣言1, ..., ローカル型変数宣言Q>
 where 型制約1
 ...
 where 型制約R
{
  ...
}

この構文に従って、Foo を宣言し、具体化したのが次の例です。TOwner は TCar から推論で求められるので、パラメータ型変数ではなくローカル型変数として宣言してあります。Foo はパラメータを 1 つ持つクラスとなっており、パラメータを 2 つ持つクラスではありません。Foo<Car<Teacher>> と具体化すると、TCar = Car<Teacher> となり、型制約から TOwner = Teacher が求まります。

class Foo<TCar><TOwner>
  where TCar : Car<TOwner>
  where TOwner : Person
{
  TCar car;
  TOwner owner;
}
Foo<Car<Teacher>> foo;

ここまではおさらいでしたが、ここからはローカル型変数を導入するために発生する厄介ごとに関する話です。前の例では、具体化の際に TOwner が具体的な型に確定しましたが、確定しない場合もあります(pon さんが指摘している問題の1つです)。

次の例では、X を具体化して変数 x を宣言していますが、その変数を使った式 x.b の具体的な型は求まりません。TB に何の型制約もかせられていないので当然です。この例では、x.b に対する全ての操作に矛盾しない一貫した型であれば、どんな型を選んでもよいことを意味しています。

class X<TA><TB>
{
  TB b;
}
X<String> x;

次の例でも、Y を具体化して変数 y を宣言していますが、その変数を使った式 y.b の具体的な型は求まりません。この例では、y.b に対する全ての操作に矛盾しない一貫した型であれば、Teacher ~ Person の間のどんな型を選んでもよいことを意味しています。

class Y<TA, TC><TB>
  where TA : TB
  where TB : TC
{
  TB b;
}
Y<Teacher, Person> y;

実際のところ、前の2つの例のように具体的な型が求まらなくても、制約に矛盾がなければ不正なプログラムにはならないので問題ありません。
ただし、今の C# にある「全てのパラメータ型変数に具体的な型を代入したクラスでなければインスタンスを作ることはできない」という言語仕様との整合性を考えると都合よくありません(このルールを変えようとすると言語仕様の修正が大きくなりすぎて、もはや別の言語になってしまいます)。
そこで、「メソッドの戻り値・引数・ローカル変数・フィールドの型として使われるローカル型変数は具体的な型が求まることを保障できなければエラーとする」というルールを追加します。
「メソッドの戻り値・引数・ローカル変数・フィールドの型として使われる」わけではないローカル型変数は、クラスのインターフェイスを記述するために直接的に使われないので、C# の言語仕様に影響を与えません。
「具体的な型が求まることを保障できる」ローカル型変数は、パラメータ型変数に代入される具体的な型の一部を抜き出したり、それを組み合わせた型になることがほとんどでしょう。

今の C# は var を除いてほとんど明示的に型を指定するので、かなり制限された状況での型推論を実装するだけで大丈夫な言語になっています。具体的には、式の構成要素から式全体へという順に型を求めることができる状況になっています(一般的な型推論では再帰関数の戻り値の型を求めるために随分と計算量が増えてしまいます)。これにより、式を構成するノード数に比例する計算量で推論できることが保障されています。

そして、ローカル型変数のための型推論も同程度の計算量で計算できます。「ローカル型変数がパラメータ型変数のどの部分を抜き出したものか」を計算するだけだからです。
この計算の中では、C# の型推論と同様に「再帰関数の戻り値の型を求める」という種類の計算が発生しないため、式を構成するノード数に比例する計算量で推論できます。

ちなみに、ローカル型変数のための型推論をどのように行うかのヒントを示します(規則をちゃんと書くとすごく大変なので)。

あるクラス X の各型制約 A : B のために、A が B へ代入可能性を判定し、代入可能なら A : B を代入可能集合に追加し、代入不可能なら X の型制約に間違いがあるとします。A や B は型変数でも具体的な型でもそれらを複合した型でも構いません。代入可能性の判定は、A と B をユニファイしながら行います。ユニファイする際に部分構造の代入可能性の判定が発生することもあります。X の全ての型制約の代入可能性を求めると型変数がユニファイされた状態になるので、パラメータ型変数にユニファイされた具体的な型の部分構造となっているローカル型変数をマークします。マークされていないローカル型変数はエラーとします。

今の C# にアルゴリズムを追加するのであれば、型制約に数に比例する計算量で実装できるので非常に高速です。

> (1)
> 例えば
>  Bar<T1> local T2 where T1 : T2
> と定義して、T1だけ型指定しT2の指定を省略する形で Bar を使った場合、
> (T1の指定が System.Object でなければ)T2の型は一意に決まりません。
> このとき、コンパイラはコンパイルエラーを発生させるべきでしょうか?
「メソッドの戻り値・引数・ローカル変数・フィールドの型として使われる」のであればコンパイルエラーとするのがよいと考えています。そうでなければコンパイルエラーを出さなくてもよいです。
ただし、T1 より上位の任意の型を T2 に代入することで型制約が常に満たせてしまいます。そのため、where T1 : T2 は型制約として機能しません。プログラマによる記述ミスの可能性があるので警告とするのがよいと考えています。

> (2)
> 例えば、mel さんの 2 番目の例のジェネリック型定義
>  Car<TOwner> ( where TOwner : Person )
> と同様に、ジェネリック型
>  Country<TOwner> ( where TOwner : Person )
> が定義されていて、さらに 3 つの型パラメータ TCar, TCountry, TOwner をもつジェネリック型
>  Foo<TCar, TCountry> local TOwner ( where TCar : Car<TOwner>, TCountry : Country<TOwner> )
> が定義されているものとします。
> Person 型の継承型 Person1, Person2 があり、プログラマが
>  Foo<Car<Person1>, Country<Person2>> tFoo = …;
> と書いた場合、コンパイラはコンパイルエラーを発生させるべきでしょうか?
コンパイルエラーとなるべきです。Foo の型制約は「TCar の型パラメータと TCounter の型パラメータがぴったり同じ型でなければならない」と言っているからです。ローカル型変数であってもパラメータ型変数と同様の検査方法で検査されるので、ローカル型変数をパラメータ型変数と置き換えて (C# の通常の generics と同じように) 考えるとよいです。

> (3)
> 例えば、mel さんの 2 番目の例のジェネリック型定義 Foo<TCar> local TOwner
> とともに、型パラメータ TCar をもつ(従来の)ジェネリック型 Foo<TCar> が定> 義されていたとして、
> プログラマが
>  Foo<Car<Person>> tFoo = …;
> と書いた場合、どちらのジェネリック型が適用されたと判断されるべきなのでしょう> か?
> (または、コンパイルエラーになるべきなのでしょうか?)
私が考えているローカル型変数の仕組みでは、パラメータ型変数の数の違いでオーバーロードが表します。つまり、Foo<TCar><TOwner> と Foo<TCar><>は競合するので、どちらかしか宣言できません。

以上で、ローカル型変数の効果や仕組について理解していただけたでしょうか?
ローカル型変数は型上での複雑なプログラムを簡略化するための1つの方法で、通常のプログラムを簡略化するための変数と同様のアプローチです。
実はこの問題はいくつのかのアプローチがあり、匿名型変数「_」の導入(prolog などで見られる)、パラメータ型変数へアクセスするための構文(C++ のテンプレートと typedef の組み合わせでできる)の導入などがあります。

pon · 2011-08-29 22:19

えっと・・・
C#は静的型付けの言語であり、全てのオブジェクトの型は(実行時ではなく)コンパイル時に決定されます。
メンバ変数の型、メソッドの引数・返り値の型、ローカル変数の型、…etc 全てについてです。
そのアクセス修飾子(public, private, protected, internal, protected internal)は関係ありません。とにかく全てです。

> 実際のところ、前の2つの例のように具体的な型が求まらなくても、制約に矛盾がなければ不正なプログラムにはならないので問題ありません。

この文章を読んで、mel さんの案が上記の大原則(コンパイル時の型決定性)を破ろうとしているように思えたのですが・・・。
もしそうであれば、既存の基本的な性質を破壊するような仕様変更は、殆どのC#プログラマに受け入れられないと思います。
もしそうでなければ、

> 次の例では、X を具体化して変数 x を宣言していますが、その変数を使った式 x.b の具体的な型は求まりません。TB に何の型制約もかせられていないので当然です。
> この例では、x.b に対する全ての操作に矛盾しない一貫した型であれば、どんな型を選んでもよいことを意味しています。
>
> class X<TA><TB>
> {
> TB b;
> }
> X<String> x;

上記コードについて、コンパイラが x.b の型をコンパイル時に決定することになりますが、
どのようなルールに従いどんな型に決定するのでしょうか?
(コンパイラはいくつもある型の候補から、なんらかのルールに従い一つの型に決める必要があります。
 もしMicrosoftがこの案を採用・実装するのであれば、Microsoftはこの場合の型決定のルールについてもC#仕様書に記述しなければなりません。
 そしてプログラマは、var と同様に、ローカル型変数の記述を省略してもそれが実際に何であるかについて常に意識すべきだと考えます。)

あと、確認なのですが・・・
mel さんの「ローカル型変数」の指定は、省略してもしなくてもOKなんですよね?
(上記の例であれば、明示的に X<String><int> x; と書くこともできるんですよね?)

また、この「ローカル型変数」は、単にC#にだけ導入しようということではなく、
ILレベルでも同等の機能を導入しようということでいいんですよね?
(最初私は、var のような一種の糖衣構文の類の話であり、C#言語仕様およびC#コンパイラだけの拡張という捉え方をしていたのですが、
 コンパイル後のIL表現として「ローカル型変数」の情報が含まれていないと機能しない話と気づきました。)

pon · 2011-08-29 22:58

1 点質問です。

インターフェイス IHoge が定義されており、
しかしこのインターフェイスを実装する型は定義されていないものとします。

ローカル型変数を含まないジェネリック型
 class Foo<T1, T2>
  where T2 : IHoge, new()
 {
 }
を定義し、その型の引数をとるジェネリックメソッド、例えば
 void DoSomething<T1, T2>( Foo<T1, T2> foo )
  where T2 : IHoge, new()
 {
 }
を定義することが可能です。

では、ローカル型変数を含むジェネリック型
 class Foo<T1><T2>
  where T2 : IHoge, new()
 {
 }
を定義し、その型の引数をとるジェネリックメソッド、例えば
 void DoSomething<T1><T2>( Foo<T1><T2> foo )
  where T2 : IHoge, new()
 {
 }
を定義することが可能でしょうか。

コメントを残す

メールアドレスが公開されることはありません。