2010年10月11日月曜日

C# 魔改造 02 - 言語内 DSL -

書き易さと読み易さのせめぎ合い?

登場が 2001 年だから、C# はもうすぐ 10 周年なのだなーと振り返りつつ。

大目標は相互運用。初めからマネージド/アンマネージドコードが混じり合えたし、P/Invoke で API の直叩きも簡単。COM とのやり取りも RCW が良しなにしてくれる。CLS に則っていさえすれば、VB .NET や C++/CLI で書かれたクラスやメソッドも何の苦労も無く利用できる。

2.0 ではコンパイル時埋め込み系の Generics 導入。Nullable 型で 値型か null を扱えるようになった。partial クラスで自動生成向けの部分と人が書く部分を別ファイルで扱えるように。あと匿名メソッドも。

3.0 ではラムダ式や拡張メソッドが扱えるようになって、関数型言語のパラダイムも取り込んでみたり。型推論や匿名型も入った。LINQ?なにそれおいしいの?

4.0 になると dynamic 型で Python や Ruby みたいな動的型付言語とも簡単に繋がるようになる。COM とのやり取りもさらに便利になった。PLINQ?もう for 文書いたら負けなのかも…。

こんな感じでなかなか盛りだくさんな言語仕様になってます (^_^;)。相互運用という大目標と、並列コンピューティングへの対応やむなしで、公式で今後も順次魔改造されていくはず。
なので、せめてライブラリ側で統一感を出したり、紋切り型の処理を簡単に呼び出せると良いかな?と。いくつかサンプル、並べてみました。



以下の記事/ライブラリを使用/参考にさせていただいてます。ありがたく使わせていただきますm(_ _)m
moq - Project Hosting on Google Code
 http://code.google.com/p/moq/
DynamicProxy :: Castle Project
 http://www.castleproject.org/dynamicproxy/index.html
C# 3.0 Supplemental Library: Achiral - NyaRuRuの日記
 http://d.hatena.ne.jp/NyaRuRu/20080115/p1
takeshik's linx at master - GitHub
 http://github.com/takeshik/linx
進化するアーキテクチャーと新方式の設計: 流れるようなインターフェース
 http://www.ibm.com/developerworks/jp/java/library/j-eaed14/?ca=drs-jp
プログラミング言語 Scala Wiki - トップページ
 http://www29.atwiki.jp/tmiya/pages/1.html



1. 3 項演算子(?: 演算子)
まずは null チェックして…って処理が毎回出てくるので。

NotDefault
サンプル

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// null チェックして null の場合はデフォルト値を返すバージョン。
Console.WriteLine("Value1: {0}", new A().NotDefault(a => a.B).NotDefault(b => b.Value));
Console.WriteLine("Value2: {0}", new A(new B()).NotDefault(a => a.B).NotDefault(b => b.Value));
Console.WriteLine("Value3: {0}", new A(new B(10)).NotDefault(a => a.B).NotDefault(b => b.Value));
/* 実行結果
*
* Value1: 0
* Value2: 0
* Value3: 10
*
*/


// null チェックして null の場合は何もしないバージョン。
new A().NotDefault(a => a.B).NotDefault(b => Console.WriteLine("Value1: {0}", b.Value));
new A(new B()).NotDefault(a => a.B).NotDefault(b => Console.WriteLine("Value2: {0}", b.Value));
new A(new B(10)).NotDefault(a => a.B).NotDefault(b => Console.WriteLine("Value3: {0}", b.Value));
/* 実行結果
*
* Value2: 0
* Value3: 10
*
*/
}
}

class A
{
public A() { }
public A(B b) { B = b; }
public B B { get; set; }
}

class B
{
public B() { }
public B(int value) { Value = value; }
public int Value { get; set; }
}

public static class My
{
public static S NotDefault<T, S>(this T obj, Func<T, S> f)
{
return EqualityComparer<T>.Default.Equals(obj, default(T)) ? default(S) : f(obj);
}

public static void NotDefault<T>(this T obj, Action<T> a)
{
if (!EqualityComparer<T>.Default.Equals(obj, default(T))) a(obj);
}
}
}





2. 複数の as キャスト
連続で as キャストしながら条件分岐する場合、使わない変数のスコープが有効になってしまうので。

As
サンプル

using System;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
NormalAs(4);
NormalAs(4d);
NormalAs("4");
try
{
NormalAs(4m);
}
catch (NotSupportedException e)
{
Console.WriteLine(e.Message);
}
/* 実行結果
*
* Result: 16
* Result: 8
* Result: Hello, 4
* 指定されたメソッドはサポートされていません。
*
*/


MyAs(4);
MyAs(4d);
MyAs("4");
try
{
MyAs(4m);
}
catch (NotSupportedException e)
{
Console.WriteLine(e.Message);
}
/* 実行結果
*
* Result: 16
* Result: 8
* Result: Hello, 4
* 指定されたメソッドはサポートされていません。
*
*/
}

static void NormalAs(object o)
{
// 通常版。
var ni = default(int?);
var nd = default(double?);
var s = default(string);
if ((ni = o as int?) != null)
{
Console.WriteLine("Result: {0}", ni * ni);
}
else if ((nd = o as double?) != null)
{
Console.WriteLine("Result: {0}", nd + nd);
}
else if ((s = o as string) != null)
{
Console.WriteLine("Result: {0}", "Hello, " + s);
}
else
{
throw new NotSupportedException();
}
}

static void MyAs(object o)
{
// DSL 版。
o.
As<int?>().Do(ni => Console.WriteLine("Result: {0}", ni * ni)).
As<double?>().Do(nd => Console.WriteLine("Result: {0}", nd + nd)).
As<string>().Do(s => Console.WriteLine("Result: {0}", "Hello, " + s)).
As().Do(_o => { throw new NotSupportedException(); });
}
}

public static class My
{
static class Default
{
public static readonly EmptyAs EmptyAs = new EmptyAs(null);
}

static class Default<T>
{
public static readonly EmptyAs<T> EmptyAs = new EmptyAs<T>(null);
}

public static As<T> As<T>(this object o)
{
return o == null ? Default<T>.EmptyAs : new As<T>(o);
}

public static As As(this object o)
{
return o == null ? Default.EmptyAs : new As(o);
}
}

public class As
{
protected readonly object o;
public As(object o)
{
this.o = o;
}

public virtual void Do(Action<object> action)
{
action(o);
}
}

public class EmptyAs : As
{
public EmptyAs(object o)
: base(o)
{
}

public override void Do(Action<object> action)
{
}
}

public class As<T> : As
{
public As(object o)
: base(o)
{
}

public virtual object Do(Action<T> action)
{
if (o is T)
{
action((T)o);
return null;
}
else
{
return o;
}
}
}

public class EmptyAs<T> : As<T>
{
public EmptyAs(object o)
: base(o)
{
}

public override void Do(Action<object> action)
{
}

public override object Do(Action<T> action)
{
return null;
}
}
}





3. ローンパターン(Loan Pattern)
IDisposable なクラスであれば標準で using があるけど、それ以外でも統一的な構文があると良いかと。

使い終わったら消える一時ファイル
サンプル

using System;
using System.IO;
using Microsoft.VisualBasic.FileIO;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
My.UsingTempFile(tempFile =>
{
using (var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write))
using (var streamWriter = new StreamWriter(fileStream))
{
streamWriter.WriteLine("Hello, Internal DSL !!");
}

using (var fileStream = new FileStream(tempFile, FileMode.Open, FileAccess.Read))
using (var streamReader = new StreamReader(fileStream))
{
Console.WriteLine(streamReader.ReadToEnd());
}

throw new ApplicationException();
});
/* 実行結果(※エラーが発生してもファイルは削除される)
*
* Hello, Internal DSL !!
*
* TestCase 'M:ConsoleApplication1.Program.Main(System.String[])' failed: アプリケーションでエラーが発生しました。
* System.ApplicationException: アプリケーションでエラーが発生しました。
* Program.cs(28,0): 場所 ConsoleApplication1.Program.<Main>b__0(String tempFile)
* Program.cs(45,0): 場所 ConsoleApplication1.My.UsingTempFile(Action`1 action)
* Program.cs(14,0): 場所 ConsoleApplication1.Program.Main(String[] args)
*
*/
}
}

public static class My
{
public static void UsingTempFile(Action<string> action)
{
string tempFile = Path.GetFileNameWithoutExtension(FileSystem.GetTempFileName()) + ".txt";
try
{
action(tempFile);
}
finally
{
try
{
File.Delete(tempFile);
}
catch { }
}
}
}
}



使い終わったらアンロードされる AppDomain
サンプル

using System;
using System.Reflection;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Domain Name: {0}", AppDomain.CurrentDomain.FriendlyName);
My.UsingNewDomain(() =>
{
Console.WriteLine("Domain Name: {0}", AppDomain.CurrentDomain.FriendlyName);
});
/* 実行結果(※エラーが発生しても AppDomain は(ry)
*
* Domain Name: ConsoleApplication1.exe
* Domain Name: NewDomain
*
*/
}
}

public class MarshalByRefAction : MarshalByRefObject
{
public MarshalByRefAction(Action action)
{
Action = action;
}

public Action Action { get; private set; }

public void Do()
{
if (Action != null) Action();
}
}

public static class My
{
public static void UsingNewDomain(Action action)
{
var domain = default(AppDomain);
try
{
domain = AppDomain.CreateDomain("NewDomain", null, AppDomain.CurrentDomain.SetupInformation);
var marshalByRefAction =
(MarshalByRefAction)domain.CreateInstanceAndUnwrap(
typeof(MarshalByRefAction).Assembly.FullName,
typeof(MarshalByRefAction).FullName,
true,
BindingFlags.Default,
null,
new object[] { action },
null,
null,
null);
marshalByRefAction.Do();
}
finally
{
if (domain != null)
AppDomain.Unload(domain);
}
}
}
}



使い終わったら元に戻る Singleton オブジェクト
サンプル

using System;
using System.Reflection;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Singleton.Instance.Action();
Singleton.Instance.Action();
My.UsingMockSingleton(new MockSingleton(), () =>
{
// ここでは Mock に入れ替わっている。
Singleton.Instance.Action();
});
Singleton.Instance.Action();
/* 実行結果
*
* This is real object !! 48649253
* This is real object !! 48649253
* This is mock object !!
* This is real object !! 48649253
*
*/
}
}

public static class My
{
public static void UsingMockSingleton(Singleton mock, Action action)
{
var last = Singleton.Instance;
var instanceField = typeof(Singleton).GetField("instance", BindingFlags.Static | BindingFlags.NonPublic);
try
{
instanceField.SetValue(null, mock);
action();
}
finally
{
instanceField.SetValue(null, last);
}
}
}

public class Singleton
{
protected Singleton() { }
static Singleton instance = new Singleton();
public static Singleton Instance { get { return instance; } }

public virtual void Action()
{
Console.WriteLine("This is real object !! {0}", this.GetHashCode());
}
}

public class MockSingleton : Singleton
{
public MockSingleton()
{
}

public override void Action()
{
Console.WriteLine("This is mock object !!");
}
}
}





4. 匿名型の型推論
List<T> とかの Generics な型に匿名型を指定したい場合に必要…というか匿名型冷遇され過ぎな気がががが(´・ω・`)
ちなみに、このサンプルで出てくるような、「インターフェースのメソッドをデリゲートで入れ替えられるようなもの」をいくつも作るのが面倒になって思いついたのが NAnonym の LocalClass だったり。

匿名型から Generics な型を生成
サンプル

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var array = new[] { new { Key = 1, Value = "aaaa" }, new { Key = 3, Value = "cccc" }, new { Key = 2, Value = "bbbb" } };
var list = My.CreateList(array[0]);
list.AddRange(array);
list.Insert(1, new { Key = 4, Value = "dddd" });
list.Add(new { Key = 5, Value = "eeee" });

list.ForEach(item =>
{
Console.WriteLine(item);
});
/* 実行結果
*
* { Key = 1, Value = aaaa }
* { Key = 4, Value = dddd }
* { Key = 3, Value = cccc }
* { Key = 2, Value = bbbb }
* { Key = 5, Value = eeee }
*
*/

var comparer = My.CreateComparer(array[0], (x, y) => x.Key - y.Key);
list.Sort(comparer);

list.ForEach(item =>
{
Console.WriteLine(item);
});
/* 実行結果
*
* { Key = 1, Value = aaaa }
* { Key = 2, Value = bbbb }
* { Key = 3, Value = cccc }
* { Key = 4, Value = dddd }
* { Key = 5, Value = eeee }
*
*/
}
}

public static class My
{
public static List<T> CreateList<T>(T obj)
{
return new List<T>();
}

public static IComparer<T> CreateComparer<T>(T obj, Func<T, T, int> comparer)
{
return new DelegateComparer<T>(comparer);
}
}

public class DelegateComparer<T> : IComparer<T>
{
public static readonly Func<T, T, int> DefaultComparer = (x, y) => Comparer<T>.Default.Compare(x, y);

Func<T, T, int> comparer;

public DelegateComparer()
: this(null)
{
}

public DelegateComparer(Func<T, T, int> comparer)
{
this.comparer = comparer == null ? DefaultComparer : comparer;
}

#region IComparer<T> Member

public int Compare(T x, T y)
{
return comparer(x, y);
}

#endregion
}
}





すみません…だいぶ長くなりましたのでとりあえずこの辺で。順次構文拡大中なので、また面白いものがあれば紹介させていただきます。ところで、個人的には、Scala の名前渡しパラメータ がすごく欲しかったり。見栄えがきれいになるのだよー。~(´ー`~)