C#でも構文を最大限悪用してDSLを定義したい
はじめに
ディレクトリのような階層構造をC#で簡潔に表現できないかと構文を最大限悪用して試行錯誤した結果です。
作例
var result = await "C:\\aaa" with { _ = new _ { await "bbb" with { _ = new _ { "ddd" } }, await "ccc" with { _ = new _ { "eee", "fff" } } } }; Show(result);
上記のような何となく木構造にも見えなくもないコードを書くと以下のような結果を得られます。
C:\aaa C:\aaa\bbb C:\aaa\bbb\ddd C:\aaa\ccc C:\aaa\ccc\eee C:\aaa\ccc\fff
それにしてもstring
をawait
したりwith
式を呼び出し始めたりして意味わかんないですよね。
ここからはどうやってこのコードを合法としているのかを解説します。
await
出来るstring
知っている人にとっては常識かもしれませんが、async/await構文はTask
やTask<T>
専用ではありません。
一定の条件を満たすクラスや構造体であれば何でもawait
出来ます。
- インスタンスメソッドや拡張メソッドで
GetAwaiter
を実装している GetAwaiter
から返される型が以下の要件を満たしてるpublic bool IsCompleted { get; }
を実装しているpublic ??? GetResult()
を実装しているINotifyCompletion
経由でpublic void OnCompleted(Action continuation)
を実装している
というわけで、まずは拡張メソッド経由でstring
にGetAwaiter
を生やします。
internal static class StringExtension { internal static StringPathAwaiter GetAwaiter(this string @this) { return new StringPathAwaiter(@this); } }
そうしたらStringPathAwaiter
を上記の条件を満たすように実装します。
internal class StringPathAwaiter : INotifyCompletion { private readonly string _path; internal StringPathAwaiter(string path) { _path = path; } public bool IsCompleted { get { return false; } } public void OnCompleted(Action continuation) { continuation(); } public Path GetResult() { return new Path(_path); } }
こうすることでstring
をawait
することで最終的にStringPathAwaiter
経由でPath
クラスを生成しています。
謎のコレクションクラス_
それぞれのPath
には複数の子Path
を持たせたいので、先にコレクションクラスをでっちあげます。
C#ではIEnumerable
を実装して1つの引数を持つメソッドAdd
が実装されていればコレクション初期化子を使用することができます。
この辺を悪用して極限まで目立たないクラス名のコレクションクラスをでっちあげます。
internal class _ : IEnumerable<Path> { private List<Path> _backing = new List<Path>(); public void Add(Path path) { _backing.Add(path); } public IEnumerator<Path> GetEnumerator() { return _backing.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public static implicit operator List<Path>(_ x) { return x._backing; } }
Path
レコードの実装
C#ではレコード型として実装すればwith式で指定されたプロパティを変更できます。*1
また、with
の優先順位はawait
よりも低いので、見た目が悪くなるかっこが不要になりよりスタイリッシュさを演出できます。
子Path
を格納するプロパティ名を_
にして、コード上で最大限目立たなくします。
また、string
からの暗黙の型変換を定義することで、型推論が使える場面ではawait
を経由しないで直接変換出来るようにします。
internal record Path { public string Name { get; } internal Path(string value) { Name = value; } public override string ToString() { return Name; } public static Path operator /(Path x, string y) { return new Path(IO::Path.Combine(x.Name, y)); } public IEnumerable<Path> _ { set; get; } = Enumerable.Empty<Path>(); public static implicit operator Path(string path) { return new Path(path); } }
まとめ
上記のようなコードを書けば、まともに生きていればお目にかかることのないC#を書くことができます。
プロダクションコードに投入すればいろんな意味で楽しいと思うで、ぜひとも真似してみてください。