Scalaのmatch式(4)

要素+属性のマッチングに挑戦してみました.マッチングには要素の場合と同様に、case Elem(~)のコンストラクタパターンを使用します.

object Elem extends Serializable

This singleton object contains the apply and unapplySeq methods for convenient construction and deconstruction. It is possible to deconstruct any Node instance (that is not a SpecialNode or a Group) using the syntax case Elem(prefix, label, attribs, scope, child @ _*) => ...

def unapplySeq(n: Node): Option[(String, String, MetaData, NamespaceBinding, Seq[Node])]

ここで属性はMetaDataという抽象クラスで表現されています.

MetaData

ここには次のように書かれています.

This class represents an attribute and at the same time a linked list of attributes. Every instance of this class is either

・ an instance of UnprefixedAttribute key,value or
・ an instance of PrefixedAttribute namespace_prefix,key,value or
・ Null, the empty attribute list.

つまり、実際に存在するのはMetaDataを継承したUnprefixedAttribute(ネームスペースなしの属性)、PrefixedAttribute(ネームスペース付きの属性)のいずれかです.UnprefixedAttributeとPrefixedAttributeは、次の属性(へのポインタ)を持つリストのような構造をもっています.

UnprefixedAttribute
def unapply(x: UnprefixedAttribute): Some[(String, Seq[Node], MetaData)]

PrefixedAttribute
def unapply(x: PrefixedAttribute): Some[(String, String, Seq[Node], MetaData)]

これを使って次のようなサンプルを作ってみました.何かあっけなく出来てしまったような気がします.

object matchTest {

  def main(args: Array[String]): Unit = {
    val doc = <nsb:block xmlns:nsb="urn:block">
       <nsi:inline xmlns:nsi="urn:inline1" seq="1">phrase1
    <nsi:inline xmlns:nsi="urn:inline2" xmlns:nsseq="urn:attr:seq" nsseq:seq="2" seq="3">phrase2
    </nsi:inline>
    </nsi:inline>
    </nsb:block>
    val nodeSeq = doc \\ "_"
    for (node <- nodeSeq)
      node match{
      case  Elem(prefix, "inline", UnprefixedAttribute("seq", Text("1"), _), nsb, child @ _*) if (hasNs(nsb,prefix,"urn:inline1")) => 
        println("Match {urn:inline1}:inline/@seq='1' exactly!")
      case  Elem(prefix, "inline", PrefixedAttribute("nsseq","seq", Text("2"), _), nsb, child @ _*) if (hasNs(nsb,prefix,"urn:inline2")) => 
        println("Match {urn:inline2}:inline/@nsseq:seq='2' exactly!")
      case other => println("Other:{"+other.namespace+"}:"+other.label)
    }
  }
  
  def hasNs(nsb:NamespaceBinding,prefix:String,uri:String):Boolean={
    Option(nsb) match{
      case Some(nsb) => 
        if (nsb.prefix==prefix)
if (nsb.uri==uri) true else false
else hasNs(nsb.parent,prefix,uri)
      case None => false
    } 
  }
}

ちゃんと結果は

Other:{urn:block}:block
Match {urn:inline1}:inline/@seq='1' exactly!
Match {urn:inline2}:inline/@nsseq:seq='2' exactly!

と出てくれるからです.しかしこのプログラムには2つの大きな問題があります.

1. 属性を名前空間プリフィックスつきで指定している.
⇒本来は、名前空間URIで指定できるようにすべきです.

2. 属性が先頭に定義されることを前提としている.
XMLで次のように属性が挿入されればもうマッチングしなくなります.

    val doc = <nsb:block xmlns:nsb="urn:block">
       <nsi:inline xmlns:nsi="urn:inline1" style="font-weight:bold;" seq="1">phrase1
    <nsi:inline xmlns:nsi="urn:inline2" xmlns:nsseq="urn:attr:seq" style="font-style:italic;" nsseq:seq="2" seq="3">phrase2
    </nsi:inline>
    </nsi:inline>
    </nsb:block>

とすると

Other:{urn:block}:block
Other:{urn:inline1}:inline
Other:{urn:inline2}:inline

となってしまいます.

つまり属性は位置があるのでコンストラクタパターンだけでは記述しきれないということです.そこで属性の名前空間プリフィックス、属性名、値を指定してマッチングできるように工夫してみたのが次のプログラムです.

object matchTest {

  def main(args: Array[String]): Unit = {
    val doc = <nsb:block xmlns:nsb="urn:block">
       <nsi:inline xmlns:nsi="urn:inline1" seq="1">phrase1
    <nsi:inline xmlns:nsi="urn:inline2" xmlns:nsseq="urn:attr:seq" nsseq:seq="2" seq="3">phrase2
    </nsi:inline>
    </nsi:inline>
    </nsb:block>
    val nodeSeq = doc \\ "_"
    for (node <- nodeSeq)
      node match{
      case  Elem(prefix, "inline", attr, nsb, child @ _*) if (hasNs(nsb,prefix,"urn:inline1") && hasAttr("","seq","1",nsb, attr)) => 
        println("Match {urn:inline1}:inline/@seq='1' exactly!")
        println("Elem="+node)
      case  Elem(prefix, "inline", attr, nsb, child @ _*) if (hasNs(nsb,prefix,"urn:inline2") && hasAttr("urn:attr:seq","seq","2",nsb, attr)) => 
        println("Match {urn:inline2}:inline/@{urn:attr:seq}seq='2' exactly!")
        println("Elem="+node)
      case other => println("Other:{"+other.namespace+"}:"+other.label)
    }
  }
  
  def hasNs(nsb:NamespaceBinding,prefix:String,uri:String):Boolean={
    Option(nsb) match{
      case Some(nsb) => 
        if (nsb.prefix==prefix)
if (nsb.uri==uri) true else false
else hasNs(nsb.parent,prefix,uri)
      case None => false
    } 
  }

  def getPrefix(uri:String,nsb:NamespaceBinding):Option[String]={
    Option(nsb) match{
      case Some(nsb) => 
        if (nsb.uri==uri) Some(nsb.prefix) else getPrefix(uri,nsb.parent)
      case None => None
    }
  }
  
  def existsAttr(attrPrefix:String,attrName:String,attrValue:String,attr:MetaData):Boolean={
    attr match {
      case UnprefixedAttribute(name,value,next)=>
if (attrPrefix == "") 
if (name == attrName && value == Text(attrValue)) true else existsAttr(attrPrefix,attrName,attrValue,next)
else
   existsAttr(attrPrefix,attrName,attrValue,next)
      case PrefixedAttribute(prefix,name,value,next) => 
if (attrPrefix == "") 
existsAttr(attrPrefix,attrName,attrValue,next)
else
   if (attrPrefix == prefix && name==attrName && value == Text(attrValue)) true else existsAttr(attrPrefix,attrName,attrValue,next)
      case _ => false
    }
  }
  
  def hasAttr(uri:String,attrName:String,attrValue:String,nsb:NamespaceBinding,attr:MetaData):Boolean={
    if (uri == "")
    existsAttr("",attrName,attrValue,attr)
    else
    getPrefix(uri,nsb) match{
     case Some(prefix) => existsAttr(prefix,attrName,attrValue,attr) 
     case None => false
    }
  }
}

これで結果は、

Other:{urn:block}:block
Match {urn:inline1}:inline/@seq='1' exactly!
Elem=<nsi:inline seq="1" xmlns:nsi="urn:inline1" xmlns:nsb="urn:block">phrase1
    <nsi:inline nsseq:seq="2" seq="3" xmlns:nsseq="urn:attr:seq" xmlns:nsi="urn:inline2">phrase2
    </nsi:inline>
    </nsi:inline>
Match {urn:inline2}:inline/@{urn:attr:seq}seq='2' exactly!
Elem=<nsi:inline nsseq:seq="2" seq="3" xmlns:nsseq="urn:attr:seq" xmlns:nsi="urn:inline2" xmlns:nsi="urn:inline1" xmlns:nsb="urn:block">phrase2
    </nsi:inline>

となります.

Elemのような複雑な構造を持つものは、コンストラクタパターンのみでは目的のマッチングを達成できません.コンストラクタのパラーメータのオブジェクトを変数に束縛し、次のif式でこの変数を使ってマッチングすべき条件を判定させるというのが定石のようです.
勉強になりました.