読者です 読者をやめる 読者になる 読者になる

拡張メソッドと不思議なパターンマッチ

.NET

あんまり知られてなさそうな,C# 3.0 の不思議な世界.

不思議なパターンマッチ

例えば次のような拡張メソッドを考えます.

public static string Hey<T, U>(this IDictionary<T, U> dict)
    where U : IEnumerable<T>
{
    return "Hey!";
}

この拡張メソッドは,インスタンスが上記パターンに該当するときだけ使える変なインスタンスメソッドのように振る舞います.
例えば Dictionary<int, int> は上記パターンを満たさないので,Hey は IntelliSense のリストに現れません.しかし Dictionary<char, string> であればパターンを満たすので,Hey が IntelliSense のリストに現れます.

これはちょっと画期的なことです.
従来の C# では,型引数が特定条件を満たすときのみ現れるインスタンスメソッドなんて概念はありませんでした.C# 3.0 はまさにこれを可能にします.
他にも,例えば配列の次元をパターンマッチに使うことができます.

public static partial class Util
{
    public static int GetRank<T>(this T[] array)
    {
        return 1;
    }
    public static int GetRank<T>(this T[,] array)
    {
        return 2;
    }
    public static int GetRank<T>(this T[, ,] array)
    {
        return 3;
    }
}

パターンマッチが関係することで,多分,世間で思われているよりももう少しだけ不思議な力が拡張メソッドに秘められています.

さらに少し変な例

例えば次のような generic class を考えます.

public partial class Pair<X, Y>
{
    public static Pair<X, Y> Default
    {
        get { return default(Pair<X, Y>); }
    }
}

ここで,パターンマッチに優先順位があることを利用して,次のようなラベル用のクラスと拡張メソッドを定義してみます.

public sealed class Nil { }
public static partial class Util
{
    public static bool IsNil(this Pair<Nil, Nil> pair) { return true; }
    public static bool IsNil<X, Y>(this Pair<X, Y> pair) { return false; }
}

こうやってオーバーロードされた IsNil は,Pair<Nil, Nil> のときだけ true を返し,Pair の型引数がそれ以外の場合は false を返します.

var b1 = new Pair<Nil, Nil>().IsNil();            // true
var b2 = new Pair<int, Nil>().IsNil();            // false
var b3 = new Pair<int, string>().IsNil();         // false
var b4 = new Pair<int, Pair<int, Nil>>().IsNil(); // false

最後の例はちょっと面白いことになっています.ネストしていても,定義したパターンは満たしているわけですね.

さらにもう少し変な例

次のような拡張メソッドを作ってみます.

public static partial class Util
{
    public static Pair<X, Pair<Y, Z>> Push<X, Y, Z>(this Pair<Y, Z> pair, X dummy)
    {
        return Pair<X, Pair<Y, Z>>.Default;
    }
    public static Pair<X, Nil> Push<X>(this Pair<Nil, Nil> pair, X dummy)
    {
        return Pair<X, Nil>.Default;
    }
}

このメソッドは,Pair<Y, Z> と X から Pair<X, Pair<Y, Z>> を作り出します.ただし,Pair<Nil, Nil> と X の場合のみ Pair<X, Nil> を返します.

var pair =
    new Pair<Nil, Nil>()     // Pair<Nil, Nil>
        .Push(1)             // Pair<int, Nil>
        .Push("Hello")       // Pair<string, Pair<int, Nil>>
        .Push(DateTime.Now); // Pair<DateTime, Pair<string, Pair<int, Nil>>>

先ほどと同じように,ネストした型をパターンマッチで統一的に取り扱うことができています.

もっと変な例

今度はもっと変な例です.次のような interface を考えましょう.

public interface ぷよ { }
public interface 赤ぷよ : ぷよ { }
public interface 青ぷよ : ぷよ { }
public interface 緑ぷよ : ぷよ { }

ここで,Pair の再帰構造に注目し,先頭から同じ色の「ぷよ」が 4 つ以上続いている時のみ現れるインスタンスメソッドを作ってみましょう.さらに,そのメソッドを呼ぶと,先頭の 4 つの「ぷよ」は消えてしまうことにします*1.できるでしょうか?

public static partial class Util
{
    public static Pair<X, Y> 連鎖<A, X, Y>(
        this Pair<A, Pair<A, Pair<A, Pair<A, Pair<X, Y>>>>> pair)
        where A : ぷよ
    {
        return Pair<X, Y>.Default;
    }
    public static Pair<Nil, Nil> 連鎖<A>(
        this Pair<A, Pair<A, Pair<A, Pair<A, Nil>>>> pair)
        where A : ぷよ
    {
        return Pair<Nil, Nil>.Default;
    }
}

できました.

var result =
    new Pair<赤ぷよ, Nil>()
        .Push(default(赤ぷよ))
        .Push(default(青ぷよ))
        .Push(default(青ぷよ))
        .Push(default(青ぷよ))
        .Push(default(青ぷよ))
        .連鎖();
    // result の型は Pair<赤ぷよ, Pair<赤ぷよ, Nil>>

まとめ

拡張メソッドが外部のクラスにメソッドを「追加」するとき,パターンマッチが行われます.このパターンマッチを利用すると,従来不可能だった分類での,インスタンスメソッドの定義やオーバーロードが可能になります.例えば型のネストレベルが 7 段階を超えると,Obsolete 属性でコンパイルエラーになるインスタンスメソッドや,特定のパターンでネストしているときのみ使えるインスタンスメソッドを定義することができるでしょう.
まあ使い道がそんなに沢山あるかというとそこはよく分かりませんが,気分転換に遊んでみるには面白いパズルなのではないでしょうか.

注意

Generics 型をあまりネストさせすぎると,CLR が例外を発生させるので注意.

*1:この定義だと,4 つ以上続いていても,4 つ単位でしか消えない罠