一段时间以来,我们一直在维护一个处理XML和JSON数据的应用程序。 通常,维护包括修复缺陷和添加次要功能,但有时需要重构旧代码。
例如,考虑一个通过路径提取XML节点的函数:
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)
}
此功能工作正常,但需求有所变化,现在我们需要:
- 从JSON和其他类似树的数据结构中提取节点,而不仅仅是XML
- 如果找不到节点,则返回描述性错误消息
这篇文章解释了如何重构getByPath以满足新要求。
用Kleisli组成进行重构
让我们分解出一段代码,该代码创建一个函数来按名称提取子节点。 我们可以将其命名为createFunctionToExtractChildNodeByName ,但是为了简洁起见,我们将其命名为child。
val child: String => XmlNode => Option[XmlNode] =
name => node => node.child.find(_.label == name)
现在我们可以进行以下关键观察:我们的getByPath是提取子节点的函数的顺序组合,下面的代码显示了这种组合的实现:
def compose(getChildA: XmlNode => Option[XmlNode],
getChildB: XmlNode => Option[XmlNode]): XmlNode => Option[XmlNode] =
node => for {
a <- getChildA(node)
ab <- getChildB(a)
} yield ab
幸运的是, Scalaz库提供了一种更通用的方式来编写函数A => M [A],其中M是单子。 该库定义了Kleisli [M,A,B] : A => M [B]的包装器,该包装器的方法> =>以与andThe链接常规函数相同的方式链接Kleisli包装器。 我们将这种链称为Kleisli组成。 下面的代码提供了一个合成示例:
val getChildA: XmlNode => Option[XmlNode] = child(“a”)
val getChildB: XmlNode => Option[XmlNode] = child(“b”)
import scalaz._, Scalaz._
val getChildAB: Kleisli[Option, XmlNode, XmlNode] =
Kleisli(getChildA) >=> Kleisli(getChildB)
注意我们在这里使用的无点样式。 对于函数式程序员来说,将函数编写为其他函数的组合是很常见的,而从不提及它们将应用于的实际参数。
Kleisli组合正是我们实现getByPath所需要的,它是提取子节点的函数的组合。
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]作为fold的中性元素。 当path为Nil时,我们需要这个中立元素来处理特殊情况。 Kleisli.ask [Option,XmlNode]只是从任何节点到Some(node)的函数的别名。
通过XmlNode抽象
让我们概括我们的解决方案,并通过XmlNode对其进行抽象。 我们可以将其重写为以下通用函数:
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)
现在,我们可以重用此泛型函数从JSON提取节点(我们在这里使用json4s ):
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] ,用于处理JSON而不是XML,但是getByPathGeneric保持不变,并且可以处理XML和JSON。
期权抽象
我们可以进一步概括getByPathGeneric,并使用Scalaz通过Option对其进行抽象,后者提供scalaz.Monad [Option]的实例。 因此,我们可以按以下方式重写getByPathGeneric :
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)
现在,我们可以实现我们的getByPathGeneric原getByPath:
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返回错误消息。
为此,我们将使用scalaz。\ / (又名disjunction ),它是scala.Either的单声道右偏版本。 最重要的是,Scalaz为隐式类OptionOps提供了toRightDisjunction [B](b:B)方法 ,该方法将Option [A]转换为scalaz.B \ / A,从而Some(a)变为Right(a)而None变为Left (b) 。 您可以在其他博客中找到有关\ /的更多信息。
因此,我们可以编写一个函数,该函数重用getByPathGeneric ,如果找不到该节点,则返回错误消息,而不是None :
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函数仅处理XML数据,如果未找到该节点,则返回None。 我们还需要它来处理JSON并返回描述性消息,而不是None 。
我们已经看到了如何使用Scalaz提供的Kleisli组合可以排除泛型函数getByPathGeneric ,我们使用泛型(为了支持JSON)和析取(对Option进行泛化)进一步对其进行了抽象。
翻译自: https://www.javacodegeeks.com/2015/09/refactoring-with-kleisli-composition.html