働くプログラマ
マルチパラダイムと .NET (第 8 部): 動的プログラミング
Ted Neward
先月は、Microsoft .NET Framework 言語でサポートされる 3 つのメタプログラミング機能の 3 番目のパラメーター ポリモーフィズム (ジェネリック) を紹介し、構造および動作の面から可変性を提供する方法について説明しました。現在のところ、パラメーター メタプログラミングにより強力なソリューションがいくつか提供されています。しかし、パラメーター メタプログラミングは、すべての設計上の問題に対する究極の答えではありません。プログラミング パラダイムは 1 つではありません。
たとえば、先月最後のサンプルとテストベッドとして紹介した Money<> クラスを見てみましょう (図 1 参照)。このとき、通貨を型パラメーターとして使用する主な理由は、コンパイラが誤って正式な換算レートを採用せずに、ユーロをドルに換算するのを避けるためと説明しました。覚えていますか。
図 1 Money<>
class USD { }
class EUR { }
class Money<C> {
public float Quantity { get; set; }
public C Currency { get; set; }
public static Money<C> operator
+(Money<C> lhs, Money<C> rhs) {
return new Money<C>() {
Quantity = lhs.Quantity + rhs.Quantity,
Currency = lhs.Currency };
}
public Money<C2> Convert<C2>() where C2 : new() {
return new Money<C2>() { Quantity = this.Quantity,
Currency = new C2() };
}
}
先月指摘したように、こうした換算は重要なので、Convert<> ルーチンを用意してこれを実行するように設計しました。これで、ドルをユーロ、ペソ、カナダ ドルなど、換算を希望する任意の通貨に換算できます。
ただし、そのためにはなんらかの通貨換算コードが必要でしたが、図 1 を実装した時点では明らかに機能が不足しており、換算を 1 対 1 のレートで行い、Currency プロパティを新しい C2 通貨型に変換しているだけです。これで終わりにするわけにはいきません。
架空のお金の話
これを解決するには、通貨換算の計算を行うなんらかの変換ルーチンが必要ですが、多種多様な解決策が考えられます。1 つの方法は、再び継承を軸として利用し、正確に換算するよう設計したルーチンを使用して USD と EUR を ICurrency 型にします。そのため、まず、ICurrency 型を定義し、USD と EUR をそのインターフェイスの実装子としてマークします (図 2 参照)。
図 2 ICurrency 型
interface ICurrency { }
class USD : ICurrency { }
class EUR : ICurrency { }
class Money<C> where C : ICurrency {
public float Quantity { get; set; }
public C Currency { get; set; }
public static Money<C> operator+(Money<C> lhs,
Money<C> rhs) {
return new Money<C>() {
Quantity = lhs.Quantity + rhs.Quantity,
Currency = lhs.Currency };
}
public Money<C2> Convert<C2>() where C2 : new() {
return new Money<C2>() { Quantity = this.Quantity,
Currency = new C2() };
}
}
ここまではうまくいっています。実際、Money<> の型パラメーターに追加した型制約は、Money<string> 型や Money<Button> 型を使用できないようにする有効な強化策です。この手法はあまり使われないように感じますが、Java では "マーカー インターフェイス" として知られており、興味深い重要な役目を果たします。
Java では、Java 5 でこの手法に相当するカスタム属性を使用するようになるまでは、このようにして型の静的宣言を行っていました。.NET Framework ではクラスがバイト ストリームにシリアル化できることを示すのに [Serializable] を使用するのと同じように、Java クラスはメンバーを持たない Serializable インターフェイスから実装 (継承) されます。
USD と EUR を [Currency] としてマークするカスタム属性を使用したいと考えるでしょうが、カスタム属性にすると型制約を使用できません。C で型制約を使用することは重要な機能強化となるため、ここではマーカー インターフェイスを利用します。あまり使わない方法ですが、何を行えるかではなく、この型は何であるかを宣言するステートメントにする方法としてインターフェイスをとらえれば理解できます。
(ここで、Money<> インスタンスの作成が簡単になるようにコンストラクターを追加します。)
ただし、ICurrency 型で通貨換算の宣言を試みると、すぐに思わぬ問題が発生します。ICurrency 型にはサブタイプ (具体的な通貨型) に関する情報がないため、ここでは、Money<USD> 型を受け取り、換算計算を自動調整して、Money<EUR> 型に換算するメソッドを実際に宣言することができません (実際にはなんらかのインターネットを使った検索や Web サービスを実装することになりますが、今のところは静的なレートを想定します)。もし宣言できたとしても、2 つの型 (換算前通貨と換算後通貨) と 1 つのパラメーター (換算する金額) に基づいてディスパッチする必要があるため、このようなメソッドを作成しようとすると非常に複雑になります。
通貨を型として使用するのであれば、まず、次のようなメソッドを作成することになります。
interface ICurrency {
float Convert<C1, C2>(float from);
}
続いて、派生型で Convert メソッドを特殊化する方法として、次のようなコードを作成することになります。
class USD : ICurrency {
public float Convert<USD, EUR>(float from) {
return (from * 1.2f); }
public float Convert<EUR, USD>(float from) {
return (from / 1.2f); }
}
残念ながら、このコードは間違っています。コンパイラは、USD と EUR を C1 と C2 と同様の型パラメーターとして解釈します。
そこで、次のようなコードを試みるかもしれません。
class USD : ICurrency {
public float Convert<C1,C2>(float from) {
if (C1 is USD && C2 is EUR) {
}
}
}
しかし、再びコンパイラは不満を漏らします。「C1 は "型パラメーター" なのに、"変数" のように使用しています」と。つまり、C1 を型そのものとして扱うことはできません。単なるプレースホルダーです。いっこうに進みませんね。
1 つ考えられる解決策は、単純に、型をリフレクションベースの型パラメーターとして渡す方法です。つまり、図 3 に示すようなコードを作成します。
図 3 リフレクションベースの型パラメーターを使用する
interface ICurrency {
float Convert(Type src, Type dest, float from);
}
class USD : ICurrency {
public float Convert(Type src, Type dest, float from) {
if (src.Name == "USD" && dest.Name == "EUR")
return from / 1.2f;
else if (src.Name == "EUR" && dest.Name == "USD")
return from * 1.2f;
else
throw new Exception("Illegal currency conversion");
}
}
class EUR : ICurrency {
public float Convert(Type src, Type dest, float from) {
if (src.Name == "USD" && dest.Name == "EUR")
return from / 1.2f;
else if (src.Name == "EUR" && dest.Name == "USD")
return from * 1.2f;
else
throw new Exception("Illegal currency conversion");
}
}
class Money<C> where C : ICurrency, new() {
public Money() { Currency = new C(); }
public Money(float amt) : this() { Quantity = amt; }
public float Quantity { get; set; }
public C Currency { get; set; }
public static Money<C> operator +(Money<C> lhs, Money<C> rhs) {
return new Money<C>(lhs.Quantity + rhs.Quantity);
}
public Money<C2> Convert<C2>() where C2 : ICurrency, new() {
return new Money<C2>(
Currency.Convert(typeof(C), typeof(C2), this.Quantity));
}
}
これはうまくいきます。コードはコンパイルされ、実行されます。しかし、たくさんの問題が待ち構えています。USD クラスと EUR クラスの両方に換算コードをコピーしなければなりません。英国ポンド (GBP) のような新しい通貨が追加されると、ご想像のとおり、新しい GBP クラスが必要になるだけでなく、USD クラスと EUR クラスにも GBP クラスを含めるように変更を加える必要があります。これではすぐに厄介な問題になります。
大切なのは何か
従来のオブジェクト指向プログラミング (OOP) 言語では、開発者は仮想メソッドを使用して 1 つの型を基にディスパッチできました。コンパイラは、呼び出されるメソッドの参照元となる実際の型に応じて、適切なメソッド実装に要求を送ります (たとえば、従来の ToString シナリオなどです)。
しかし、今回の状況では、2 つの型 (C1 と C2) に基づいてディスパッチすることを求めています。これは、二重ディスパッチとも呼ばれます。従来の OOP でこれを行うには、ビジター (Visitor) 設計パターン以外に適切なソリューションがありません。正直なところ、多くの開発者にとっては、これも十分なソリューションとは言えません。このパターンを使用するには、作成するクラス階層の目的を 1 つにする必要があります。つまり、新しい型が導入されると、メソッドで階層全体を作り直し、新しい型に対応できるようにする必要があります。
一歩下がって、問題点をあらためて見直してみます。Money<USD> インスタンスと Money<EUR> インスタンスが混在しないようにするにはタイプセーフ性が必要ですが、実際には、型パラメーターとしての役割を大幅に超える USD 型と EUR 型は必要ありません。つまり、通貨換算という目的を果たすうえで考慮すべきは型ではなく、単純に名前です。名前は、別の形式の可変性を可能にします。これを、名前バインドや動的プログラミングと呼びます。
動的言語と動的プログラミング
一見、動的言語と動的プログラミングには本質的な関係があるように思えます。確かに、ある程度は関係はありますが、動的言語が名前バインド実行の概念を採用する場合のみ両者の関係が密になります。Ruby、Python、JavaScript などの動的言語は、コンパイル時に対象のメソッドやクラスが存在するかどうかを確認するのではなく、単純にこれらが存在することを前提として、できるだけ最後の段階でその存在を確認します。
もちろん、ご存知のように、.NET Framework ではバインドでの同等の柔軟性を、リフレクションを使用して、経験豊富な設計者に提供しています。通貨の名前を含む静的クラスを作成し、リフレクションを使用してそのクラスを呼び出すことができます (図 4 参照)。
図 4 リフレクションによる動的バインド
static class Conversions {
public static Money<EUR> USDToEUR(Money<USD> usd) {
return new Money<EUR>(usd.Quantity * 1.2f);
}
public static Money<USD> EURToUSD(Money<EUR> eur) {
return new Money<USD>(eur.Quantity / 1.2f);
}
}
class Money<C> where C : ICurrency, new() {
public Money() { Currency = new C(); }
public Money(float amt) : this() { Quantity = amt; }
public float Quantity { get; set; }
public C Currency { get; set; }
public static Money<C> operator +(Money<C> lhs, Money<C> rhs) {
return new Money<C>(lhs.Quantity + rhs.Quantity);
}
public Money<C2> Convert<C2>() where C2 : ICurrency, new() {
MethodBase converter = typeof(Conversions).GetMethod(
typeof(C).Name + "To" + typeof(C2).Name);
return (Money<C2>)converter.Invoke(null, new object[] { this });
}
}
英国ポンドなど、新しい通貨が追加されると、単純に (ICurrency 型を実装する) 空の GBP クラスを作成し、Conversions クラスに必要な変換ルーチンを追加します。
もちろん、C# 4 (およびこれ以前の Visual Basic のほぼすべてのバージョン) には、コンパイル時に名前を把握していることを前提として、この処理を簡略化する組み込みの機能が用意されています。C# では動的な型を使用でき、Visual Basic には何十年もの間、Option Strict Off と Option Explicit Off が用意されています。
実際のところ、Apple の Objective-C で示されるように、動的プログラミングは必ずしもインタープリター言語に限定されるわけではありません。Objective-C は、フレームワーク内のあらゆる場所で動的プログラミングを使用するコンパイル言語で、特に、イベント処理のバインド時に使用します。イベントを受け取るクライアントは、適切な名前を付けたイベント処理メソッドを指定するだけです。送信元がクライアントに興味深い情報を提供するときは、名前でメソッドを探し、見つかればそのメソッドを呼び出します (昔のことを覚えている方であれば、これはまさに Smalltalk のしくみでもあります)。
もちろん、名前バインドによる解決にはデメリットもあり、そのほとんどは、エラー処理に関係します。存在するはずのメソッドやクラスが存在しないとどうなるでしょう。Smalltalk や Apple が実装した Objective-C など、単に何もしない言語もあります。また、Ruby のように、エラーや例外をスローすべきだと提案している言語もあります。
正しい答えはドメイン自体によって異なります。Money<> の例では、特定の通貨に換算できないと合理的に想定される場合は、存在しない換算ルーチンからユーザーになんらかのメッセージが表示されます。ただし、システム内の通貨をすべて換算できるとしても、明らかに開発者による間違いがあれば、この間違いは単体テスト時にキャッチされるべきです。実際、動的プログラミング ソリューションは、重要な一連の単体テストに合格することなく、無防備なユーザーにリリースされることはないと考えるのが妥当です。
共通性を生み出す
名前バインドによる可変性は、共通性/可変性分析の強力なメカニズムですが、動的プログラミングはそれだけではありません。CLR に存在する完全な忠実性があるメタデータ機能を使用すると、名前以外の条件 (メソッドの戻り値の型、メソッドのパラメーター型など) を通じて共通性を生み出せるようになります。実際のところ、属性メタプログラミングは、カスタム属性に基づく動的プログラミングの単なる派生物であると主張されています。そのうえ、名前バインドによる可変性では、名前全体を結び付ける必要はありません。NUnit 単体テスト フレームワークの初期ビルドでは、テスト メソッドが "test." という文字列から始まることを前提としていました。
次回は、.NET Framework の共通言語の最後のパラダイムとして、関数型プログラミングを調査し、関数型プログラミングでは、共通性/可変性分析を確認する方法がどのようにして提供されるかについて説明します。これは、従来のオブジェクト指向プログラミングで見受けられるものとまったく正反対です。
Ted Neward は、Microsoft .NET Framework および Java のエンタープライズ プラットフォーム システムを専門とする独立企業 Neward & Associates の社長を務めています。これまでに 100 個を超える記事を執筆している Ted は、C# MVP であり、INETA の講演者でもあります。さまざまな書籍を執筆および共同執筆していて、『Professional F# 2.0』(Wrox 2010 年、英語) もその 1 つです。彼は定期的にコンサルティングを行い、開発者を指導しています。彼の連絡先は ted@tedneward.com (英語のみ) です。ブログを blogs.tedneward.com (英語) に公開しています。
この記事のレビューに協力してくれた技術スタッフの Mircea Trofin に心より感謝いたします。