働くプログラマ
Gemini ライブラリを使用して動的プログラミングに移行する
私のコラムやブログ記事の読者の皆さんは既にご存じだと思いますが、偶然から、または星座占いだと思ってこのコラムをご覧になった方のために説明すると、私は長い時間を割いて他のプログラミング言語やプラットフォームについて説明することがよくあります。通常、このような説明の目的は、ソフトウェアのモデル作成の効果、効率、または正確さを高めるうえで役立つ概念やアイデアを追求することです。
最近の (とは言えそれほど最近でもありませんが) Web コミュニティの動向として、"動的" 言語または "スクリプト" 言語の追求が挙げられます。中でも注目を集めている 2 つの言語が、Ruby (この言語のために Ruby on Rails (RoR) フレームワークが作成されています) と JavaScript (サーバー側のアプリケーションをこの言語で実行するために Node.js やその他さまざまなフレームワークが用意されています) です。どちらの言語も、オブジェクト構成要素に関する唯一の定義としてクラスの定義を厳密に遵守するという、C# と Visual Basic の世界ではおなじみの規則がないことが特徴です。
たとえば JavaScript (私のような知識をひけらかしたがる発表者が "中かっこを使用した Lisp" と位置付ける言語) の場合、オブジェクトは完全に変更可能なエンティティです。つまり、プロパティやメソッドを臨機応変に追加できます。
var myCar = new Object();
myCar.make = "Ford";
myCar.model = "Mustang";
myCar.year = 1969;
myCar.makeSounds = function () {
console.log("Vroom! Vroom!")
}
最初に作成した時点では、この myCar オブジェクトにはプロパティもメソッドも含まれていません。プロパティやメソッドは、データ値 ("Ford"、"Mustang"、1969、および function) をその名前 (make、model、year、および makeSounds) に設定したときに追加されます。基本的に、JavaScript の各オブジェクトは名前と値のペアで構成された辞書で、ペアの値は呼び出し予定のデータ要素か関数のどちらかになります。言語設計者は、このような型情報の処理機能をよくメタオブジェクト プロトコル (MOP) と呼び、この機能を一部制限したものをアスペクト指向プログラミング (AOP) と呼んでいます。MOP はオブジェクトに対する柔軟で強力な手法ですが、C# とはまったく異なります。従来の C# オブジェクト設計で提案されているように、継承を介してすべてのバリエーションを網羅しようとする複雑なクラス階層を作成する代わりに、MOP 手法では、現実世界にある物事は (もちろんそのデータを除いて) 1 つとしてまったく同じものはなく、これらをモデル化する方法も同じにはならないとしています。
Microsoft .NET Framework コミュニティに長年参加してきた開発者は、以前のバージョンの C# で導入された、実行時にメンバーが検出されるオブジェクトへの参照を宣言できる dynamicキーワード/型を思い出されるかもしれませんが、これは別の問題です (この動的な機能セットを使用すると簡単にリフレクション スタイルのコードを記述できますが、MOP 風のオブジェクトは作成できません)。さいわい C# 開発者は、標準の C# クラスの設計メカニズムを使用した従来の静的な型定義と、動的機能を基盤としていて JavaScript に似た機能を提供する Gemini というオープン ソース ライブラリを使用した柔軟な型定義の、両方のオプションを利用できます。
Gemini の基本
このコラムで説明したことがある他の多数のパッケージと同じく、Gemini は NuGet から入手できます。パッケージ マネージャー コンソールで Install-Package Gemini を実行すると、Gemini の機能がプロジェクトにインストールされます。しかし、これまで紹介してきた他のパッケージとは違い、Gemini をプロジェクトにインストールする場合、アセンブリは 1 つもインストールされません。代わりに、複数のソース ファイルが "Oak" という名前のフォルダーに配置され、プロジェクトに直接追加されます (このコラムの執筆時点では、Gemini 1.2.7 は Gemini.cs、GeminiInfo.cs、ObjectExtensions.cs、およびリリース ノートが記載されたテキスト ファイルの 4 ファイルで構成されています)。フォルダーに Oak という名前が付いていることには、もっともな理由があります。それは、Gemini が ASP.NET MVC の世界にこの動的プログラミングの機能を多数提供するより大規模なプロジェクト (もちろん名前は Oak) のサブセットであるという理由です。大規模な Oak パッケージについては、今後のコラムで説明します。
Gemini がソースとして提供されるという事実自体はそれほど重要ではありません。コードが独自の名前空間 (Oak) に存在し、残りのソース ファイルと同様に、プロジェクトにコンパイルされるだけです。しかし実際の作業では、ソース ファイルがあると、問題が起きたときに Gemini ソース コードをステップ実行することや利用できる内容の確認用にコードをざっと確認することがかなり簡単になります。これは、dynamic キーワード/型を使用すると IntelliSense がまったく動作しなくなることがあるためです。
開始する
今回もいつものとおり、いくつかの調査テストを記述する単体テスト プロジェクトの作成から始めます。このプロジェクトに Gemini をインストールし、以下のような、"hello world" に似た単純なテストを作成してテストします。
[TestMethod]
public void CanISetAndGetProperties()
{
dynamic person = new Gemini(
new { FirstName = "Ted", LastName = "Neward" });
Assert.AreEqual(person.FirstName, "Ted");
Assert.AreEqual(person.LastName, "Neward");
}
ここで行っている処理はわずかですが、強力です。"person" 参照の反対側にある Gemini オブジェクトは、(前述のコードのように) プロパティやメソッドを割り当てるか以下のように SetMember メソッドや GetMember メソッドを使用して明示的にメンバーを追加しない限り、基本的にメンバーがまったくない型です。
[TestMethod]
public void CanISetAndGetPropertiesDifferentWays()
{
dynamic person = new Gemini(
new { FirstName = "Ted", LastName = "Neward" });
Assert.AreEqual(person.FirstName, "Ted");
Assert.AreEqual(person.LastName, "Neward");
person = new Gemini();
person.SetMember("FirstName", "Ted");
person.SetMember("LastName", "Neward");
Assert.AreEqual(person.GetMember("FirstName"), "Ted");
Assert.AreEqual(person.GetMember("LastName"), "Neward");
}
ここではデータ メンバーをテストしていますが、動作メンバー (メソッド) も同じように簡単にテストできます。そのためには、(void を返す) DynamicMethod または (値を返すことを想定する) DynamicFunction のインスタンスをメンバーに設定します。DynamicMethod も DynamicFunction も、パラメーターを受け取りません。また、メソッドまたは関数でパラメーターを受け取ることができる場合は、以下のように動作メンバーを "WithParam" バージョンに設定できます。
[TestMethod]
public void MakeNoise()
{
dynamic person =
new Gemini(new { FirstName = "Ted", LastName = "Neward" });
person.MakeNoise =
new DynamicFunction(() => "Uh, is this thing on?");
person.Greet =
new DynamicFunctionWithParam(name => "Howdy, " + name);
Assert.IsTrue(person.MakeNoise().Contains("this thing"));
}
ところで、Gemini ライブラリでは少し重要な処理が実行されます。Gemini オブジェクト (代替実装がまったくないオブジェクト) では、"構造化された型指定" を使用して、オブジェクトが等しいかどうかや特定の実装を満たしているかどうかを判断します。継承/IS-A テストを使用してオブジェクト パラメーターの型に関する制限を特定のオブジェクトが満たしているかどうかを判断する OOP 型のシステムとは違い、構造化された型指定のシステムでは、渡されたオブジェクトについて、そのオブジェクトがコードの正常な実行に必要なすべての要件 (この場合はメンバー) を満たしているかどうか問い合わせます。構造化された型指定は、関数型言語ではこの名前で通っていますが、動的言語ではダック タイピングとも呼ばれています。
ここで、オブジェクトを受け取ってそのオブジェクトに関するわかりやすいメッセージを表示するメソッドについて考えてみましょう (図 1 参照)。
図 1 オブジェクトを受け取ってメッセージを表示するメソッド
string SayHello(dynamic thing)
{
return String.Format("Hello, {0}, you are {1} years old!",
thing.FirstName, thing.Age);
}
[TestMethod]
public void DemonstrateStructuralTyping()
{
dynamic person = new Gemini(
new { FirstName = "Ted", LastName =
"Neward", Age = 42 });
string message = SayHello(person);
Assert.AreEqual("Hello, Ted, you are 42 years old!",
message);
dynamic pet = new Gemini(
new { FirstName = "Scooter", Age = 3, Hunter = true });
string otherMessage = SayHello(pet);
Assert.AreEqual("Hello, Scooter, you are 3 years old!",
otherMessage);
}
通常、ソフトウェア システムで人間とペットが共有する属性は (ネコはどう思っているか知りませんが) それほど多くないので、従来のオブジェクト指向の階層では、Person (人間) と Pet (ペット) は継承ツリーの大きく異なった部分に由来することになります。しかし、構造化された型指定 (ダック タイピング) のシステムでは、継承チェーンを深く伸ばしてすべてのメンバーを含めるために必要な労力が減ります。狩りも行う人間がいる場合、人間に "Hunter" (ハンター) メンバーを含めることができます。人間、ネコ、またはプレデター ドローンのいずれのオブジェクトを渡す場合でも、そのオブジェクトが Hunter かどうかを確認する任意のルーチンでこのメンバーを使用できます。
問い合わせる
多くの方がお気付きになるでしょうが、ダック タイピング手法のトレードオフは、特定の種類のオブジェクトだけを渡せるようコンパイラで制御できなくなることです。Gemini の型にも同じことが当てはまります。これは主に、ほとんどの Gemini コードでは慣用的にオブジェクトが動的参照に隠されるためです。開発者は渡されるオブジェクトが要件を満たすようにもう少し時間と労力を投入する必要があります。これを怠るとランタイム例外が発生します。具体的には、オブジェクトに必要なメンバーが含まれているかどうか確認するためにそのオブジェクトる必要があり、Gemini ではこの処理に RespondsTo メソッドを使用します。他にも、一部のメソッドは、Gemini によって特定のオブジェクトの一部として認識されるさまざまなメンバーを返します。
たとえば、狩りの方法が設定されたオブジェクトを想定している以下のようなメソッドについて考えてみましょう。
int Hunt(dynamic thing)
{
return thing.Hunt();
}
Scooter を渡すと、このメソッドは適切に動作します (図 2 参照)。
図 2 動的プログラミングが動作する場合
[TestMethod]
public void AHuntingWeWillGo()
{
dynamic pet = new Gemini(
new
{
FirstName = "Scooter",
Age = 3,
Hunter = true,
Hunt = new DynamicFunction(() => new Random().Next(4))
});
int hunted = Hunt(pet);
Assert.IsTrue(hunted >= 0 && hunted < 4);
// ...
}
しかし、狩りの方法が設定されていないオブジェクトを渡すと、例外が発生します (図 3 参照)。
図 3 動的プログラミングが動作しない場合
[TestMethod]
public void AHuntingWeWillGo()
{
// ...
dynamic person = new Gemini(
new
{
FirstName = "Ted",
LastName = "Neward",
Age = 42
});
hunted = Hunt(person);
Assert.IsTrue(hunted >= 0 && hunted < 4);
}
これを防ぐには、Hunt メソッドで RespondsTo メソッドを使用して、対象となるメンバーが存在しているかどうかテストする必要があります。このテストは TryGetMember メソッドの簡単なラッパーで、単純なブール値のはい/いいえ型応答を目的としています。
int Hunt(dynamic thing)
{
if (thing.RespondsTo("Hunt"))
return thing.Hunt();
else
// If you don't know how to hunt, you probably can't catch anything.
return 0;
}
ところで、これが Dictionary<string,object> のごく単純な定型コードやラッパーに思える場合、その評価は間違いではありません。Gemini クラスはまさに Dictionary インターフェイスを基盤としているからです。しかし、ラッパー型を使用すると、dynamic キーワードの使用と同様に本来なら必要になる型システムの変換を、一部簡略化できます。
しかし、複数のオブジェクトが同じような動作を共有している場合は何が起きるでしょうか。たとえば、4 匹のネコすべてが狩りの方法を知っている (4 つのオブジェクトに狩りの方法が設定されている) 場合、特に 4 匹ともネコ科の狩りの習性を共有しているときは、4 匹のネコすべてについて新しい匿名メソッドの定義を記述するといくぶん効率が落ちるでしょう。従来の OOP では、どのネコも Cat クラスのメンバーとして同じ実装を共有するので、これは問題になりません。JavaScript などの MOP システムの場合は、通常、別のオブジェクトに対するプロパティや要求の呼び出しをオブジェクトで遅延または "連鎖" できるようにする "プロトタイプ" と呼ばれるメカニズムがあります。Gemini では、静的な型指定と "拡張" と呼ばれる MOP との興味深い組み合わせを使用します。
プロトタイプ
まず、ネコを定義する以下のような基本型が必要です。
public class Cat : Gemini
{
public Cat() : base() { }
public Cat(string name) : base(new { FirstName = name }) { }
}
Cat クラスが Gemini を継承していることに注目してください。この継承を行っているからこそ、ここまで説明してきた動的な柔軟性すべてを Cat クラスで利用できます。事実、2 つ目の Cat コンストラクターでは、これまですべての動的インスタンスの作成で使用してきたコンストラクターと同じ、Gemini のコンストラクターを使用しています。つまり、ここまでのコードはすべての Cat インスタンスにも適用できます。
ただし、Gemini では Cat を "拡張" する方法も宣言できるため、各インスタンスに明示的に追加しなくても、すべての Cat インスタンスに同じ機能を付与できます。
クラスを拡張する
この機能の実際の使用方法を説明するために、開発中の Web アプリケーションがあるとします。多くの場合は、HTML インジェクション (またはより深刻な SQL インジェクション) を誤って許してしまわないよう、格納して返す名前の値を HTML に対してエスケープする必要があります。
string Htmlize(string incoming)
{
string temp = incoming;
temp = temp.Replace("&", "&");
temp = temp.Replace("<", "<");
temp = temp.Replace(">", ">");
return temp;
}
システムで定義されたすべてのモデル オブジェクトでこの処理を実行することは面倒ですが、さいわいにして MOP ではモデル オブジェクトの新しい動作メンバーを体系的に "検索" して定義できます (図 4 参照)。
図 4 メソッドを記述しないメソッドの記述
[TestMethod]
public void HtmlizeKittyNames()
{
Gemini.Extend<Cat>(cat =>
{
cat.MakeNoise = new DynamicFunction(() => "Meow");
cat.Hunt = new DynamicFunction(() => new Random().Next(4));
var members =
(cat.HashOfProperties() as IDictionary<string, object>).ToList();
members.ForEach(keyValuePair =>
{
cat.SetMember(keyValuePair.Key + "Html",
new DynamicFunction( () =>
Htmlize(cat.GetMember(keyValuePair.Key))));
});
});
dynamic scooter = new Cat("Sco<tag>oter");
Assert.AreEqual("Sco<tag>oter", scooter.FirstName);
Assert.AreEqual("Sco<tag>oter", scooter.FirstNameHtml());
}
基本的に、Extend の呼び出しでは末尾に "Html" を付加した新しいメソッドを すべての Cat 型に追加するので、FirstNameHtml メソッドを呼び出せば FirstName プロパティの HTML セーフ バージョンにアクセスできます。
また、システムのどの Gemini 継承型でも、この処理全体を実行時に実行できます。
永続化などの機能
Gemini は決して、動的に解決される多数のオブジェクトで C# の環境全体を置き換えることを目的に設計されたものではありません。Gemini の基本的な用途は、Oak MVC フレームワーク内で、永続化などの便利な動作を (とりわけ) モデル クラスに追加することと、ユーザーのコードを煩雑にせず部分クラスも必要としない検証機能を追加することです。ただし、Gemini は Oak の外部でも強力な設計メカニズムとしての役割を果たします。これについては、以前にこのコラムで連載していた「マルチパラダイムと .NET」シリーズの第 8 部 (msdn.microsoft.com/magazine/hh205754) で見覚えがある読者もいらっしゃるでしょう。
Oak については次回のコラムで取り上げる予定ですので、この動的フレームワークがどのように現実世界のシナリオで役に立つか、引き続きご注目ください。
コーディングを楽しんでください。
Ted Neward は Neward & Associates LLC の社長を務めています。これまでに 100 本を超える記事を執筆している Ted は、さまざまな書籍を執筆および共同執筆していて、『Professional F# 2.0』(Wrox、2010 年) もその 1 つです。F# MVP、有名な Java 専門家でもあり、世界中の Java と .NET 両方のカンファレンスで講演をしています。彼は定期的にコンサルティングを行い、開発者を指導しています。彼の連絡先は ted@tedneward.com (英語のみ) です。また、彼がチームの作業に加わることに興味がある場合の連絡先は、Ted.Neward@neudesic.com (英語のみ) です。彼のブログは blogs.tedneward.com (英語) で、Twitter は twitter.com/tedneward (英語) でフォローすることができます。
この記事のレビューに協力してくれた技術スタッフの Amir Rajan (Improving Enterprises) に心より感謝いたします。
Amir Rajan は、Improving Enterprises の主任コンサルタントです。Rajan は開発コミュニティのメンバーとして積極的に活動し、ASP.NET MVC、HTML5、REST アーキテクチャ、Ruby、JavaScript/CoffeeScript、NodeJS、iOS/ObjectiveC、および F# に関する専門知識を持っています。Rajan は、ソフトウェアに確固たる情熱を注ぐ真の多言語使用者です。Twitter は @amirrajan (英語) からアクセスでき、Web では github.com/amirrajan (英語)、amirrajan.net (英語)、および improvingenterprises.com (英語) から連絡を取ることができます。