F#のmatch式

久しぶりにVisual Studioを立ち上げて、F#での要素+属性のマッチングを試してみました.

この前Scalaでやってみた教訓は、

1.XMLパターンがあるが、どうもネームスペースがちゃんとサポートされていないみたい.
2.XMLパターンには属性までマッチングさせる記述ができない.
3.コンストラクタパターンでも一発ではゆかない.パターンガードでやたら条件をつけてやる必要がある.

というものでした.イマイチですね.ではF#はどうなのでしょうか?F#のパターンマッチについてはMSDNの以下の箇所で詳しく解説されています.

パターン マッチ (F#)

ぱっと見ただけではF#にはScalaXMLパターンもコンストラクタパターンもありません.でもF#にはScalaにはない強力な武器があります.それはアクティブパターンです.

アクティブパターン(機械翻訳なのでイマイチの気もします)

Active patterns enable you to define named partitions that subdivide input data, so that you can use these names in a pattern matching expression just as you would for a discriminated union.

実践F# 関数型プログラミング入門から引用させていただきました.

「レコードや判別共用体は構造的な型であり、その構造を示すパターンを持っています.クラスは構造的な型ではないのでパターンを持っていません.ですがアクティブパターン(active pattern)と呼ばれる機能を使うことで、クラスをパターンマッチにかけることができます.アクティブパターンとは、パターンとして使用できる関数を定義する機能のことです.バナナクリップと呼ばれる、(|...|)で囲ってパターンを定義します.」(実践F# 関数型プログラミング入門 荒井省三 いげ太 著 技術評論社 p.292より)

F#はScalaとは考え方が違って、コンストラクタパターンではなく、クラスインスタンスから判別に必要な部分の値を切り取り提供する関数をアクティブパターンで定義できる訳です.こっちの方がスマートに思えます.

以前Scalaのところで使ったXMLをそのまま使って試してみたのが以下の例です.

open System.Linq
open System.Xml
open System.Xml.XPath
open System.Xml.Linq
open System.Text
open System.Collections.Generic

// XML source
let xmlDoc = 
    let tempXml = 
        "<?xml version=\"1.0\" encoding=\"utf-8\"?>
        <nsb:block xmlns:nsb=\"urn:block\">
            <nsi:inline xmlns:nsi=\"urn:inline1\" seq=\"1\">phrase1
       <nsi:inline xmlns:nsi=\"urn:inline2\" seq=\"2\">phrase2
    </nsi:inline>
    </nsi:inline>
    </nsb:block>"
    let tempXmlDoc=XDocument.Parse(tempXml)
    tempXmlDoc

// Mach an XML element name
let (|Element|_|) name (inp: XElement) =
    if inp.Name.LocalName = name then Some(inp)
    else None

// Match an XML element namespace
let (|Namespace|_|) ns (inp: XElement) =
    if inp.Name.NamespaceName = ns then Some(inp)
    else None

// Get the attributes of an element
let (|Attributes|) (inp: XElement) = inp.Attributes()

// Match a specific attribute
let (|Attr|) attrName (inp: IEnumerable<XAttribute>) =
    let first = inp.First(fun attribute -> (attribute.Name = XName.op_Implicit(attrName)))
    match first with
    | null -> failwith (attrName + " not found")
    | attribute -> attribute.Value

[<EntryPoint>]
let main argv = 
    let inlineElements = xmlDoc.XPathSelectElements("//*[local-name()='inline']")
    let showElem =
        for inlineElement in inlineElements do
            match inlineElement with
            | Element "inline" (Namespace "urn:inline1" (Attributes (Attr "seq" "1"))) -> printfn "Match {urn:inline1}:inline[ @seq=\"1\"] exactly! %s" (inlineElement.ToString()) 
            | Element "inline" (Namespace "urn:inline2" (Attributes (Attr "seq" "2"))) -> printfn "Match {urn:inline2}:inline[ @seq=\"2\"] exactly! %s" (inlineElement.ToString()) 
            | _ -> printf "Not match"
    0 // return an integer exit code

これを実行した結果が以下になります.

イメージ 1

アクティブパターンのElementは指定した名前が要素のlocal-name()に一致する場合その要素を返します.ですので

  | Element "inline" e -> printfn "Match local-name()=\"inline\"! %s" (inlineElement.ToString()) 

としてやれば、2つのinline要素にマッチします.

イメージ 2


アクティブパターンのNamespaceは指定した名前が要素のネームスペースURIに一致する場合その要素を返します.ですので

  | Element "inline" (Namespace "urn:inline1" e) -> printfn "Match {urn:inline1}:inline exactly! %s" (inlineElement.ToString()) 
  | Element "inline" (Namespace "urn:inline2" e) -> printfn "Match {urn:inline2}:inline exactly! %s" (inlineElement.ToString()) 

としてやれば、ネームスペースまで含めてマッチングさせられます.

イメージ 3

そしてアクティブパターンのAttributesとAttrを使えば属性値まで指定してマッチングできます.

  | Element "inline" (Namespace "urn:inline1" (Attributes (Attr "seq" "1"))) -> printfn "Match {urn:inline1}:inline[@seq=\"1\"] exactly! %s" (inlineElement.ToString()) 
  | Element "inline" (Namespace "urn:inline2" (Attributes (Attr "seq" "2"))) -> printfn "Match {urn:inline2}:inline[@seq=\"2\"] exactly! %s" (inlineElement.ToString()) 

いや、アクティブパターンすごいです!Scalaよりずっとスマートにmatch式を記述できます.ScalaとF#でXMLを処理するプログラムを作れと言われたら、まちがいなくF#じゃないかと思わせてくれますね.