Scala の match と XML リテラルをまねる

Scala では次のようなことが可能です。

def proc(node: Node) {
  node match {
    case <hoge>{value}</hoge> => println(value)
  }
}

proc(<hoge>100</hoge>)  // => 100

これを C# で真似てみました。
Scala では入れ子など色々とできるのですが、そこまでは対応できていません。


使う側はこんな感じです。

static class Program
{
    static Node Node(string name) { return new Node(name); }

    static void Proc(XElement elem)
    {
        // こんな感じに使う
        elem.Match(
            Node("hoge").Attr("a1", "a2").Then((a1, a2, es) =>
                Console.WriteLine("({0},{1}),{2}", a1, a2, es.First().Value) ),

            Node("hoge").Attr("a").Then((a, es) =>
                Console.WriteLine("{0},{1}", a, es.First().Value) ),

            Node("hoge").Then(es =>
                Console.WriteLine(es.First().Value) )
        );
    }

    static void Main(string[] args)
    {
        // a1はあるがa2がないので最後にマッチする
        Proc(XElement.Load(new StringReader("<hoge a3='3' a1='bar'>100</hoge>")));
        // aがあるので真ん中にマッチする
        Proc(XElement.Load(new StringReader("<hoge a3='3' a='0'>100</hoge>")));
        // aもあるが、a1とa2が両方あるので最初のにマッチする
        Proc(XElement.Load(new StringReader("<hoge a3='3' a='0' a2='foo' a1='bar'>100</hoge>")));
    }
}

ここで、各 Then メソッドがそれぞれ

  • Action>
  • Action>
  • Action>

を受け取っていますが (具体的な型は出てきてないので、ラムダ式の引数の数が違う、という点だけに注目してください)、例えば

Node("hoge").Attr("a1", "a2").Then(es => {})

とすると、コンパイルエラーになるようにしています。
これはメソッドのオーバーロードで実現しているのですが、拡張メソッドと併用することで流れるように指定できるようにしています。
以下実装です。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;

// 長ったらしいので別名を用意しておく
using Act0 = Action<IEnumerable<XElement>>;
using Act1 = Action<string, IEnumerable<XElement>>;
using Act2 = Action<string, string, IEnumerable<XElement>>;
using XElems = IEnumerable<XElement>;

// てきとーなインターフェイス
public interface IMatcher
{
    bool ActIfMatch(XElement elem);
}

// IMatcherの実装はジェネリック
// 利用者からはジェネリックな部分は意識する必要なし
public class Matcher<T> : IMatcher
{
    protected readonly string NodeName;
    internal Matcher(string nodeName)
    {
        NodeName = nodeName;
        Attrs = new string[0];
    }

    public bool ActIfMatch(XElement elem)
    {
        if (elem.Name != NodeName)
            return false;
        if (Attrs.Any(a => elem.Attribute(a) == null))
            return false;

        // 子要素がない場合が微妙・・・
        var elems = elem.Elements().Skip(1);
        var isEmpty = elems.Count() == 0;
        Act(Attrs.Select(a => elem.Attribute(a).Value), isEmpty ? new[] { elem } : elems);
        return true;
    }

    internal Action<IEnumerable<string>, XElems> Act { get; set; }
    internal string[] Attrs { get; set; }
}

// 自分自身はMatcher<Act0>
public class Node : Matcher<Act0>
{
    public Node(string name) : base(name) { }

    // Attr(string)を呼び出すとMatcher<Act1>を返し、
    public Matcher<Act1> Attr(string name)
    {
        return new Matcher<Act1>(NodeName) { Attrs = new[] { name } };
    }

    // Attr(string, string)を呼び出すとMatcher<Act2>を返す
    public Matcher<Act2> Attr(string name1, string name2)
    {
        return new Matcher<Act2>(NodeName) { Attrs = new[] { name1, name2 } };
    }
}

// 型によって異なる引数のメソッドに振り分ける
public static class MatcherExtension
{
    // Matcher<Act0>、つまりNode、つまり属性なしの場合
    public static Matcher<Act0> Then(this Matcher<Act0> m, Act0 act)
    {
        m.Act = (strs, es) => act(es);
        return m;
    }

    // Matcher<Act1>、つまり属性が1つの場合
    public static Matcher<Act1> Then(this Matcher<Act1> m, Act1 act)
    {
        m.Act = (strs, es) => act(strs.First(), es);
        return m;
    }

    // Matcher<Act2>、つまり属性が2つの場合
    public static Matcher<Act2> Then(this Matcher<Act2> m, Act2 act)
    {
        m.Act = (strs, es) => act(strs.ElementAt(0), strs.ElementAt(1), es);
        return m;
    }
}

public static class XElementExtension
{
    // マッチしたら登録されているアクションを実行!
    public static void Match(this XElement elem, params IMatcher[] matchers)
    {
        foreach (var m in matchers)
        {
            // マッチしたら登録されているアクションを実行して終了
            if (m.ActIfMatch(elem))
                return; // ここでbreak;と書くかreturn;と書くかは好みが分かれる?
        }
    }
}

あとはこれを T4 に落として属性 10 個くらいまで作ればそれなりに使えるんじゃないでしょうか?
それよりも入れ子に対応するのが先・・・?