getByPath
function, which retrieves an element from the XML tree by its full path. import scala.xml.{Node => XmlNode} def getByPath(path: List[String], root: XmlNode): Option[XmlNode] = path match { case name::names => for { node1 <- root.child.find(_.label == name) node2 <- getByPath(names, node1) } yield node2 case _ => Some(root) }
getByPath
function refactored to meet the new requirements.createFunctionToExtractChildNodeByName
, but let's call it just a child
for short. val child: String => XmlNode => Option[XmlNode] = name => node => node.child.find(_.label == name)
getByPath
function is a sequential composition of functions that retrieve children. The following compose function implements this composition of two functions: getChildA
and getChildB
. type ExtractXmlNode = XmlNode => Option[XmlNode] def compose(getChildA: ExtractXmlNode, getChildB: ExtractXmlNode): ExtractXmlNode = node => for {a <- getChildA(node); ab <- getChildB(a)} yield ab
A => M[A]
, where M is a monad . The library defines Kleisli[M, A, B]
, a wrapper for A => M[B]
, which has a method> => to implement a sequential composition of these Kleisli
, like the composition of ordinary functions using andThen
. This composition we will call composition Kleisli . The code below shows an example of such a composition: val getChildA: ExtractXmlNode = child(“a”) val getChildB: ExtractXmlNode = child(“b”) import scalaz._, Scalaz._ val getChildAB: Kleisli[Option, XmlNode, XmlNode] = Kleisli(getChildA) >=> Kleisli(getChildB)
getByPath
function as a composition of child
functions that retrieve children. import scalaz._, Scalaz._ def getByPath(path: List[String], root: XmlNode): Option[XmlNode] = path.map(name => Kleisli(child(name))) .fold(Kleisli.ask[Option, XmlNode]) {_ >=> _} .run(root)
Kleisli.ask[Option, XmlNode]
as a neutral element of the fold method. We need this neutral element to handle the special case where path is empty. Kleisli.ask[Option, XmlNode]
is just another designation of a function from any node in Some(node)
.getByPathGeneric
: def getByPathGeneric[A](child: String => A => Option[A]) (path: List[String], root: A): Option[A] = path.map(name => Kleisli(child(name))) .fold(Kleisli.ask[Option, A]) {_ >=> _} .run(root)
getByPathGeneric
to retrieve an item from JSON (we use json4s here): import org.json4s._ def getByPath(path: List[String], root: JValue): Option[JValue] = { val child: String => JValue => Option[JValue] = name => json => json match { case JObject(obj) => obj collectFirst {case (k, v) if k == name => v} case _ => None } getByPathGeneric(child)(path, root) }
child: JValue => Option[JValue]
, to work with JSON instead of XML, but the getByPathGeneric
function remained unchanged and works with both XML and JSON.getByPathGeneric
even more and abstract it from Option
using the Scalaz libraries, which provides an instance of the monad for the Option -- scalaz.Monad[Option]
. So we can rewrite getByPathGeneric
as follows: import scalaz._, Scalaz._ def getByPathGeneric[M[_]: Monad, A](child: String => A => M[A]) (path: List[String], root: A): M[A]= path.map(name => Kleisli(child(name))) .fold(Kleisli.ask[M, A]) {_ >=> _} .run(root)
getByPath
function using the getByPathGeneric
function: def getByPath(path: List[String], root: XmlNode): Option[XmlNode] = { val child: String => XmlNode => Option[XmlNode] = name => node => node.child.find(_.label == name) getByPathGeneric(child)(path, root) }
getByPathGeneric
to return an error message if the item is not found. For this we use scalaz. \ / (The so-called “disjunction”) which is the right-hand version of scala.Either
.Scalaz
provides the “implicit” (implicit) OptionOps
class with the toRightDisjunction[B](b: B)
method, which converts Option[A]
to scalaz.B\/A
, so that Some(a)
becomes Right(a)
and None
becomes Left(b)
.getByPathGeneric
to return an error message instead of None
if the item we are looking for is not found. type Result[A] = String\/A def getResultByPath(path: List[String], root: XmlNode): Result[XmlNode] = { val child: String => XmlNode => Result[XmlNode] = name => node => node.child.find(_.label == name).toRightDisjunction(s"$name not found") getByPathGeneric(child)(path, root) }
getByPath
function processed only data in XML format and returned None if the item was not found. We needed it to also work with the JSON format and return an error message instead of None.Scalaz
library allows you to write a generalized getByPathGeneric
function, using parameterized types (generics) to support both XML and JSON, as well as scalaz. \ / (Disjunction) to abstract from Option
and issue messages about mistakes.Source: https://habr.com/ru/post/304622/
All Articles