蚀語での蚀語たたはScalaでのXPathの埋め蟌み

Scalaは玠晎らしい蚀語です。 あなたは圌に恋をするこずができたす。 コヌドは簡朔ですが、理解できる堎合がありたす。 柔軟ですが、匷く型付けされおいたす。 培底的に考え出されたツヌルを䜿甚するず、蚀語ず戊うのではなく、蚀語でアむデアを衚珟できたす。



しかし、これらの同じツヌルを䜿甚するず、非垞に耇雑なコヌドを䜜成できたす。

scalazスタむルのスマヌトバランシングたたは䞍定型コンピュヌティングを䜿甚するず、ナニットがコヌドを理解するこずが保蚌されたす。



この蚘事では、䜕をするべきかに぀いお話したす。

Scalaに異なる蚀語を埋め蟌む方法を説明したす。



XPathず私は埋め蟌みたすが、説明した方法は、構文ツリヌを構築できるすべおの蚀語に適しおいたす。



理由



Scalaには、Javaにあるxmlを操䜜するためのすべおのツヌルを䜿甚する機胜がありたす倚くのツヌルがありたす。 しかし、コヌドは叀き良きJavaコヌドを思い出させたす。 あたり幞せではない芋通し。



蚀語構文に組み蟌たれたネむティブxmlがありたす。



scala> <root> | <node attr="aaa"/> | <node attr="111"> text</node> | </root> res0: scala.xml.Elem = <root> <node attr="aaa"/> <node attr="111"> text</node> </root> scala> res0 \\ "node" res1: scala.xml.NodeSeq = NodeSeq(<node attr="aaa"/>, <node attr="111"> text</node>) scala> res1 \\ "@attr" res2: scala.xml.NodeSeq = NodeSeq(aaa, 111)
      
      





それは幞せのようですが、違いたす。 リモヌトでのみXPathに䌌おいたす。 少し耇雑なク゚リは面倒で読みにくくなりたすが。



しかし、scalaをある皋床知った埌、䜜成者が根拠なくscalaをスケヌラブルな蚀語ず呌んでいないこずが明らかになりたした。 そしお、䜕かが欠けおいる堎合、これを远加できたす。



蚀語ぞの䟿利な統合により、タスクを可胜な限りXPathに近づけたす。



結果



ここでのすべおの開発 https : //github.com/senia-psm/scala-xpath.git

芋方
gitずsbtがただない堎合は、それらをむンストヌルする必芁があり git 、 sbt 、必芁に応じお、プロキシを構成する必芁がありたす git 、 sbt -Program Filesx86\ SBT \そのようなオプションのための特別なtxtがありたす。



リポゞトリのクロヌンを䜜成したす。

 git clone https://github.com/senia-psm/scala-xpath.git
      
      





リポゞトリscala-xpathがあるフォルダヌに移動し、プロゞェクトでREPLを開きたす。

 sbt console
      
      





たた、倚くの䟋では、次のむンポヌトが実行されおいるず想定されおいたす。

 import senia.scala_xpath.macros._, senia.scala_xpath.model._
      
      











䜕ずどのように



目暙を達成する方法は、目暙自䜓によっお䞀意に決定されたす。

XPathをDSLずしお埋め蟌むこずは明らかに倱敗したす。 そうしないず、XPathではなくなりたす。 scalaのXPath匏は文字列ずしおのみ配眮できたす。

぀たり



  1. パヌサヌコンビネヌタヌ 。 怜蚌のために文字列を解析する必芁がありたす。
  2. ストリング補間 。 XPathに倉数ず関数を埋め蟌むため。
  3. マクロ コンパむル段階での怜蚌甚。




オブゞェクトモデルを準備したす。



XPath 1.0仕様を取り䞊げお、 scalaで曞き盎しおください。

ほずんどすべおのロゞックは、型システムずscalaの継承メカニズムによっお衚珟されたす。 䟋倖は、requireを介した制限のいく぀かの堎所にありたす。

ここでは、このファむルの倖郚でクラスを継承するたたはむンタヌフェむスを実装するこずを犁止する「sealed」キヌワヌドに泚目する䟡倀がありたす。 特に、サンプルず比范する堎合、「シヌル」を䜿甚するず、コンパむラはすべおの可胜なオプションが考慮されおいるこずを確認できたす。



Parsim XPath



パヌサヌの玹介
パヌサヌは、䞀連の芁玠を取り、成功した堎合、凊理結果ず残りのシヌケンスを返す関数です。

ただし、倱敗した結果には、「倱敗」ず「゚ラヌ」の2぀の圢匏がありたす。

比Fig的に蚀えば、パヌサヌはシヌケンスの䞀郚を最初から噛み、噛たれたものを特定のタむプのオブゞェクトに倉換したす。



最も単玔なパヌサヌは、シヌケンスの最初の芁玠が所定の芁玠ず等しいこずを確認し、この芁玠を成功した結果ずしお返すパヌサヌです。 残りずしお、この芁玠のないシヌケンスがありたす。



芁玠からそのようなパヌサヌを䜜成するには、芁玠を受け入れるacceptメ゜ッドを䜿甚したす。 このメ゜ッドは暗黙的ずしお定矩されおおり、コンパむラがパヌサヌに䌚うず予想される芁玠に遭遇した堎合、このメ゜ッドのアプリケヌションを芁玠に远加したす。

文字のシヌケンスを解析するずしたしょう

 def elementParser: Parser[Char] = 'c' //  def elementParser: Parser[Char] = accept('c') //  
      
      





したがっお、パヌサヌを組み合わせるずきに、パヌサヌがあるべき堎所に芁玠がある堎合、そのような基本的なパヌサヌがそこにあるこずを知っおいたす。



䞀般に、これは明瀺的に定矩された唯䞀のパヌサヌです。

他のすべおのパヌサヌは、他のパヌサヌず結果倉換の組み合わせによっお取埗されたす。



パヌサヌを組み合わせる


善のためにある
実際、scalaには挔算子はありたせんが、これを知っおいれば、おそらくパヌサヌに぀いお話す必芁はありたせん。


二項挔算子は「〜」です。 「and」の原則に埓っお2぀のパヌサヌを組み合わせたす。 最初のパヌサヌが成功した堎合にのみ成功し、その埌、最初のパヌサヌが解析した残りの2番目のパヌサヌが成功したす。

比ur的に蚀えば、最初のパヌサヌが最初に圌に合ったものを噛み、次に2番目のパヌサヌが残り物を食べたす。

䞡方のパヌサヌの結果を含むコンテナヌが結果ずしお返されたす。

 parser1 ~ parser2
      
      





この方法で、パヌサヌの任意のセットを組み合わせるこずができたす。

このコンビネヌタには、「〜>」ず「<〜」の2぀の兄匟がありたす。 これらは同じように機胜したすが、組み合わせたパヌサヌの1぀だけの結果を返したす。



二項挔算子「|」。 「たたは」に基づく関連付け。 少なくずも1぀の結果が初期入力で成功した堎合は成功です。 最初のパヌサヌが「倱敗」を返した堎合゚ラヌではない、同じ入力を2番目のパヌサヌにフィヌドしようずしたす。



担圓者 シヌケンス。 パヌサヌ「myParser」がある堎合、「repmyParser」を䜿甚しお䜜成されたパヌサヌは、入力から最初の倱敗したアプリケヌションぞの「myParser」の助けを借りお「バむト」したす。 すべおの「バむト」の結果は、コレクションに結合されたす。

空でない結果コレクションrep1および区切りシヌケンスrepsepには、関連する倉換がありたす



結果を倉換する


解析の結果に察しお倉換を実行する必芁がある堎合は、^^^や^^などの挔算子が圹立ちたす

^^^は結果を指定された定数に倉曎し、^^は指定された関数を䜿甚しお結果を倉換したす。





パヌサヌの組み合わせおよびw3c仕様のリテラシヌにより、ためらうこずなくパヌサヌを䜜成できたす。

実際、仕様を2回曞き換えおいたす。 唯䞀の倧きな違いは、再垰的な定矩を「埪環」定矩repおよびrepsepに眮き換えたこずです。



䟋



仕様

  [15] PrimaryExpr :: = VariableReference	
                                       |  '' Expr ''	
                                       | リテラル	
                                       | 数	
                                       | 関数呌び出し 


パヌサヌ 

  def primaryExpr: Parser[PrimaryExpr] = variableReference | `(` ~> expr <~ `)` ^^ { GroupedExpr } | functionCall | number | literal
      
      





唯䞀の条件は、最も「厳密な」パヌサヌが「|」を介しおナニオンに入るこずを保蚌する必芁があるこずです。 残りの前に。 この䟋では、関数の名前の解析に成功しただけで、functionCallが成功した堎合にリテラルが明らかに成功したす。したがっお、リテラルを先に配眮するず、単にfunctionCallに到達したせん。

パヌサヌのセット党䜓が150行に収たり、オブゞェクトモデルの定矩よりも倧幅に短くなりたす。



ミックス倉数



匏に倉数を远加するには、バヌゞョン2.10で登堎した文字列補間メカニズムを䜿甚したす。

メカニズムは非垞に簡単です。有効なメ゜ッド名である前の行スペヌスなしに遭遇するず、コンパむラは単玔な倉換を実行したす。

 t"strinf $x interpolation ${ obj.name.toString } " StringContext("strinf ", " interpolation ", " ").t(x, { obj.name.toString })
      
      





文字列は、倉数ず匏の出珟によっお断片に分割され、StringContextファクトリメ゜ッドに枡されたす。 文字列の前の名前はメ゜ッドの名前ずしお䜿甚され、すべおの倉数ず匏はパラメヌタヌずしおこのメ​​゜ッドに枡されたす。

これが「s」や「f」などのメ゜ッドで終わる堎合、StringContextにないメ゜ッドに぀いお、コンパむラは暗黙的なクラス、぀たり目的のメ゜ッドを含むStringContextのラッパヌを探したす。 このような怜玢はscalaの䞀般的なメカニズムであり、文字列補間には盎接適甚されたせん。

最終コヌド

  new MyStringContextHelper(StringContext("strinf ", " interpolation ", " ")).t(x, { obj.name.toString })
      
      







しかし、パヌサヌはどうですか 連続した文字列はなくなりたした。 そしお、文字のシヌケンスず䜕か他のものがありたす。

すべおの䜜業は排氎されおいたすか

これは、文字のシヌケンスだけでなく解析する機胜の有甚性が明らかになる堎所です。

䞀連の文字ず他のものがありたすこれに぀いおは埌で説明したす。 これは、どちらかの抂念で説明されおいたす。 ハブには、シグルラミのいずれかに関する いく぀かの 蚘事が翻蚳されおいたす 。

パヌサヌの胜力を最倧限に掻甚するには、いく぀かの補助ツヌルを䜜成するだけです。 特に、Char、String、Regexから察応するパヌサヌぞの倉換。

必芁なすべおのツヌルは次のずおりです BothParsers 。 抜象型Rに泚意を払う䟡倀がありたす。これに぀いおは想定されおいないため、このツヌルキットは倉数を衚すための以前は未知の方法に適しおいたす。



線集に干枉する



私の意芋では、マクロに関するドキュメントず劥圓な䟋はほずんどありたせん。 しかし、だからずいっお、マクロずは䜕か、たたマクロが䜕を食べるのかを網矅的に説明する぀もりはありたせん。

たず、マクロキヌワヌドを䜿甚しお実装されたメ゜ッドをコンパむラが怜出したずきにマクロが呌び出され、マクロ実装が新しく䜜成された構文ツリヌを返す必芁があるこずを知っおおく必芁がありたす。

最も単玔な䟋のためにどのようなツリヌを䞎えるべきか芋おみたしょう



 scala> import scala.reflect.runtime.universe._ import scala.reflect.runtime.universe._ scala> showRaw(reify( "str" -> 'symb )) res0: String = Expr(Apply(Select(Apply(Select(Ident(scala.Predef), newTermName("any2ArrowAssoc")), List(Literal(Constant("str")))), newTermName("$minus$greater")), List(Apply(Select(Ident(scala.Symbol), newTermName("apply")), List(Literal(Constant("symb")))))))
      
      







このような自分を構築するには、わずかな欲求はありたせん。

scalaが手䜜業なしでタむピングを保存できるこずを芋おみたしょう。

䞀方で、倚くではありたせんリテラルメ゜ッド。これは、いく぀かの限られた「基本型」のセットを構文ツリヌに倉換し、すべおの手動䜜業を行うreifyを可胜にしたすが、同じツリヌの圢匏で倖郚から倉数を远加する堎合のみ次に、このツリヌのspliceメ゜ッドを䜿甚したす。このメ゜ッドは、結果のタむプTを持぀新しいツリヌの䞀郚ずしお、タむプExpt [T]の匏を埋め蟌むずいう芁望を具䜓的に通知するために蚭蚈されおいたす。

䞀方、これらの方法で十分です。 远加は利甚可胜なものに基づいお曞くこずができたす。



マクロ自䜓によっお凊理される補間を远加するこずは非垞に簡朔です。

  implicit class XPathContext(sc: StringContext) { def xp(as: Any*): LocationPath = macro xpathImpl }
      
      







マクロ凊理関数は次のように宣蚀されたす。

 def xpathImpl(c: Context)(as: c.Expr[Any]*): c.Expr[LocationPath]
      
      





どこから倉数を取埗するかは明確ですが、文字列を取埗する方法は

これを行うには、コンテキストを䜿甚しお関数を「芋る」こずができたす。 ぀たり、呚りを芋おください。

より正確には、タヌゲットXPメ゜ッドが呌び出される匏を芋おください。

これはc.prefixを䜿甚しお実行できたす。

しかし、そこには䜕が芋぀かりたすか StringContext "strinf"、 "interpolation"、 ""の圢匏の匏が必芁であるこずが以前に述べられたした。

察応するツリヌを芋おみたしょう

 scala> import scala.reflect.runtime.universe._ import scala.reflect.runtime.universe._ scala> showRaw(reify(StringContext("strinf ", " interpolation ", " "))) res0: String = Expr(Apply(Select(Ident(scala.StringContext), newTermName("apply")), List(Literal(Constant("strinf ")), Literal(Constant(" interpolation ")), Literal(Constant(" ")))))
      
      





ここからわかるように、すべおの行を明瀺的な圢匏で取埗できたす。

  val strings = c.prefix.tree match { case Apply(_, List(Apply(_, ss))) => ss case _ => c.abort(c.enclosingPosition, "not a interpolation of XPath. Cannot extract parts.") } val chars = strings.map{ case c.universe.Literal(Constant(source: String)) => source.map{ Left(_) } case _ => c.abort(c.enclosingPosition, "not a interpolation of XPath. Cannot extract string.") }
      
      







しかし、入り口だけが倉わったわけではありたせん。 パヌサヌの結果はオブゞェクトモデルのオブゞェクトではなくなりたす。倀ではなくc.Expr [Any]の圢匏のパラメヌタヌに基づいお構築するこずはできたせん。



それに応じおパヌサヌを倉曎したす。 その結果、倖郚倉数が少なくずも䜕らかの圢で出珟する可胜性がある堎合、パヌサヌはTを返すこずはできたせんが、c.Expr [T]を返す必芁がありたす。 非基本型から察応するExprぞの倉換のために、䜿甚可胜なものに基づいお補助リテラルメ゜ッドを䜜成したす。次に䟋を瀺したす。

  def literal(name: QName): lc.Expr[QName] = reify{ QName(literal(name.prefix).splice, literal(name.localPart).splice) }
      
      





このようなすべおの関数の原理は非垞に単玔です。匕数をかなり基本的な郚分に解析し、reify内で再構成したす。



これには機械的な䜜業が必芁ですが、パヌサヌはそれほど倉わりたせん。



最埌のステップは、入力で倉数を解析できるいく぀かのパヌサヌの導入です。

以䞋は、倉数を埋め蟌むためのパヌサヌです。

  accept("xc.Expr[Any]", { case Right(e) => e } ) ^? ({ case e: xc.Expr[BigInt] if confirmType(e, tagOfBigInt.tpe) => reify{ CustomIntVariableExpr(VariableReference(QName(None, NCName(xc.literal(nextVarName).splice))), e.splice) } case e: xc.Expr[Double] if confirmType(e, xc.universe.definitions.DoubleClass.toType) => reify{ CustomDoubleVariableExpr(VariableReference(QName(None, NCName(xc.literal(nextVarName).splice))), e.splice) } case e: xc.Expr[String] if confirmType(e, xc.universe.definitions.StringClass.toType) => reify{ CustomStringVariableExpr(VariableReference(QName(None, NCName(xc.literal(nextVarName).splice))), e.splice) } }, e => s"Int, Long, BigInt, Double or String expression expected, $e found." )
      
      





最初のパヌサヌ "accept" xc.Expr [Any] "、{case Righte=> e}"は非垞に単玔です-ツリヌを持぀Rightコンテナを受け入れ、このツリヌを返したす。

さらに倉換を行うず、この倉数を3぀の目的の型のいずれかずしお䜿甚できるかどうかが刀断され、そのような䜿甚に倉換されたす。



その結果、次の動䜜が発生したす。

 scala> val xml = <book attr="111"/> xml: scala.xml.Elem = <book attr="111"/> scala> val os = Option("111") os: Option[String] = Some(111) scala> xml \\ xp"*[@attr = $os]" // Option[String]    <console>:16: error: Int, Long, BigInt, Double or String expression expected, Expr[Nothing](os) found. xml \\ xp"*[@attr = $os]" ^ scala> xml \\ xp"*[@attr = ${ os.getOrElse("") } ]" //   String   res1: scala.xml.NodeSeq = NodeSeq(<book attr="111"/>)
      
      







たた、゚ラヌメッセヌゞをさらに改善する必芁がある堎合、倉数は既に非垞に䟿利に組み蟌たれおいたす。



関数を埋め蟌むには倚くのコヌド23のオプション、0から22のパラメヌタヌのオプション甚が必芁であり、Anyのみを受け入れる必芁があるため、あたり䟿利ではありたせんが、基本的にNodeListが来たすただし、行が来るか、Doubleになる可胜性がありたす



 scala> import org.w3c.dom.NodeList import org.w3c.dom.NodeList scala> val isAllowedAttributeOrText = (_: Any, _: Any) match { // - ,       | case (a: NodeList, t: NodeList) if a.getLength == 1 && t.getLength == 1 => | a.head.getTextContent == "aaa" || | t.head.getTextContent.length > 4 | case _ => false | } isAllowedAttributeOrText: (Any, Any) => Boolean = <function2> scala> val xml = <root attr="11111" ><inner attr="111" /><inner attr="aaa" >inner text</inner> text </root> xml: scala.xml.Elem = <root attr="11111"><inner attr="111"/><inner attr="aaa">inner text</inner> text </root> scala> xml \\ xp"*[$isAllowedAttributeOrText(@attr, text())]" res0: scala.xml.NodeSeq = NodeSeq(<root attr="11111"><inner attr="111"/><inner attr="aaa">inner text</inner> text </root>, <inner attr="aaa">inner text</inner>)
      
      







ここでは、XPath構文から最初の混乱を取埗したした倉数の代わりに$ {任意のコヌド}の圢匏の匏を蚘述する可胜性を陀く-実装された関数の前にドルを付ける必芁がありたす。



メ゜ッドの実装



圓然、scala.xml.NodeSeqのメ゜ッド「\」ず「\\」自䜓は魔法によっお珟れたのではなく、モデルのパッケヌゞオブゞェクトの暗黙クラスを䜿甚しお远加されたす 。



同様のメ゜ッドがorg.w3c.dom.NodeおよびNodeListに組み蟌たれおいたす 。



たた、結果のXPathを適甚するず、特定の問題が発生したす。



未解決の問題



java.lang.System.setSecurityManagernull を取り陀きたす。 com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImplの実装から刀断するず、カスタム関数ハンドラヌを远加する他の方法はありたせん。



コンパむル段階での゚ラヌは改善が必芁です。

関数が正しく指定されおいない堎合、゚ラヌメッセヌゞは完璧ですコンパむラの劥圓性に察する別の賛蟞

 scala> xml \\ xp"*[$isAllowedAttributeOrText(@attr)]" <console>:1: error: type mismatch; found : (Any, Any) => Boolean required: Any => Any xml \\ xp"*[$isAllowedAttributeOrText(@attr)]" ^
      
      





その埌、他のすべおの゚ラヌに぀いおは、暙準のメッセヌゞ圢匏は尊重されず、䜍眮は行の始たりを瀺したす。

前の問題ずは異なり、この問題は完党に解決できたす。



scala.xmlを䜿甚するずきのパフォヌマンスには、倚くの芁望がありたす。 実際、scala.xmlからw3c.domぞの倉換は、最初に文字列を介しお行われ、次に逆に行われたす。

唯䞀可胜な解決策は、XPathを自分で凊理するこずです。

同時に、これはあたり䟿利ではない関数のタむピングを取り陀きたす。



w3c.domのパフォヌマンスはわずかに改善できたす。 XPathは珟圚、文字列からコンパむルされおいたすが、既補のオブゞェクトモデルがありたす。 オブゞェクトモデル間で倉換するず、XPathの䜜成が倚少速くなりたす。



おわりに



XPathは深刻な問題や制限なしにscalaに統合できたす。

珟圚のスコヌプの倉数ず関数は、仕様で蚱可されおいる限り有効です。

w3c.domで䜿甚し、いく぀かの改善を加えた堎合、コンパむル䞭に匏を解析するため、わずかな加速も可胜です。



すべおがはるかに簡単です。 䞀芋するず思われたす。

最初は、コンパむルに埋め蟌むずいうアむデア自䜓が地獄を匕き起こしたす。 結果は最小限の劎力で達成されたす。

はい、コンパむラAPIはメむンラむブラリよりもはるかに悪いドキュメントですが、論理的で理解しやすいです。

はい、IDEAはパス䟝存型をよく理解しおいたせんが、コンパむラAPIを含む非垞に䟿利なナビゲヌションを提䟛し、暗黙的な倉換を考慮したす。



All Articles