Scalaのmatch式(3)

Scalaのコンストラクタパターンでネームスペースを含む要素のマッチングがちゃんとできないものか試してみました.まず作ったのは次のようなプログラムです.

import scala.xml.Elem
import scala.xml.NamespaceBinding

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" seq="2">phrase2
    </nsi:inline>
    </nsi:inline>
    </nsb:block>
    val nodeSeq = doc \\ "_"
    for (node <- nodeSeq)
      node match{
      case  Elem("nsi", "inline", _, NamespaceBinding("nsi", "urn:inline1", _), child @ _*)=>println("Match {urn:inline1}:inline exactly!")
      case  Elem("nsi", "inline", _, NamespaceBinding("nsi", "urn:inline2", _), child @ _*)=>println("Match {urn:inline2}:inline exactly!")
      case other => println("Other:"+other.namespace+":"+other.label)
    }
  }
}

このプログラムを走らせると、

Other:urn:block:block
Match {urn:inline1}:inline exactly!
Match {urn:inline2}:inline exactly!

と出力されます.なにかイイ感じです.コンストラクタパターンでちゃんと動いてくれているようです.このElemのコンストラクタパターンは以下のURLを参考にしました.

object Elem extends Serializable

このマッチングの仕組みについては、「第26章 抽出子 Extractors(Scala スケーラブルプログラミング p.536~)」をご覧いただきたいと思います.

しかし、どうもコンストラクタパターンのNamespaceBinding("nsi", "urn:inline1", _)、NamespaceBinding("nsi", "urn:inline2", _)という記述が大変気になります.何故かと言うと、目的のネームスペースバインディングが先頭に定義されていることを前提としているからです.実際NamespaceBindingというのはちょっと変なデータ構造で、

new NamespaceBinding(prefix: String, uri: String, parent: NamespaceBinding)

というコンストラクタを持ったケースクラスです.つまりparentで次のネームスペースをポイントしているのです.

次のようなプログラムを作ってみると

import scala.xml.Elem
import scala.xml.NamespaceBinding

object elemTest {

  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" seq="2">phrase2
    </nsi:inline>
    </nsi:inline>
    </nsb:block>
    val nodeSeq = doc \\ "_"
    for (node <- nodeSeq)
        node match{
        case  Elem(ns, name, _, nsb, child @ _*)=>println("Match:'" + ns + ":" + name + "'")
                                                  println("Namespace Binding='"+printNsb(nsb,0)+"'")
        case elem =>println("Elem:"+elem)
    }
  }
  def printNsb(nsb:NamespaceBinding,level:Int): String={
    Option(nsb) match{
      case Some(nsb) => "("+(level+1)+") "+nsb.prefix+":"+nsb.uri+","+ printNsb(nsb.parent,level+1)
      case None => "("+(level+1)+") :None"
    }
  }
}

Match:'nsb:block'
Namespace Binding='(1) nsb:urn:block,(2) null:null,(3) :None'
Match:'nsi:inline'
Namespace Binding='(1) nsi:urn:inline1,(2) nsb:urn:block,(3) null:null,(4) :None'
Match:'nsi:inline'
Namespace Binding='(1) nsi:urn:inline2,(2) nsi:urn:inline1,(3) nsb:urn:block,(4) null:null,(5) :None'

のように出力されます.つまりNamespaceBindingには使われていない接頭辞+URIの組み合わせも保存されているようです.そして、たまたまその要素のNamespaceBindingが先頭に配置されています.

最初のプログラムではNamespaceBindingの実装依存になってしまうので、プログラムを次のように変えてみました.NamespaceBindingをサーチして、最初に定義されている接頭辞に一致したらOKとするものです.

import scala.xml.Elem
import scala.xml.NamespaceBinding

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" seq="2">phrase2
    </nsi:inline>
    </nsi:inline>
    </nsb:block>
    val nodeSeq = doc \\ "_"
    for (node <- nodeSeq)
      node match{
      case  Elem(prefix, "inline", _, nsb, child @ _*)
      if (hasNs(nsb,prefix,"urn:inline1")) => println("Match {urn:inline1}:inline exactly!")
      case  Elem(prefix, "inline", _, nsb, child @ _*)
      if (hasNs(nsb,prefix,"urn:inline2")) => println("Match {urn:inline2}:inline 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
    } 
  }
}

こうすればNamespaceBindingにコンストラクタを書かずに

Other:urn:block:block
Match {urn:inline1}:inline exactly!
Match {urn:inline2}:inline exactly!

という結果を得ることができます.結局コンストラクタパターン一発でマッチングというわけには行きませんでした.

しかし、名前空間接頭辞と名前空間URIの表現をNamespaceBindingというクラス構造でやっていて、しかもその実装が不要なデータを含んでいる(nsi:urn:inline2のあるところにnsi:urn:inline1が残っている)というのは実にイマイチな気がします.

例えば、Anti-XMLでは

com.codecommit.antixml Elem


case class Elem(prefix: Option[String], name: String, attrs: Attributes, scope: Map[String, String], override val children: Group[Node]) extends Node with Selectable[Elem]

のように名前空間接頭辞と名前空間URIはMap[String, String]を使って実装されています.何かこっちの方がデータ構造としては綺麗な感じがします.

次は属性をマッチングさせる方法について調べます.