📜 ⬆️ ⬇️

Refactoring with Cleisley Composition

For quite a long time, we have supported an application that processes data in XML and JSON formats. Usually, support consists in correcting defects and slightly expanding functionality, but sometimes it also requires refactoring of old code.


Consider, for example, the 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) } 

')
This feature worked fine, but the requirements have changed and now we need:


In this article we will describe how to getByPath function refactored to meet the new requirements.

Kleisli composition


Let's select that code snippet that retrieves the child by name. We can call it createFunctionToExtractChildNodeByName , but let's call it just a child for short.

 val child: String => XmlNode => Option[XmlNode] = name => node => node.child.find(_.label == name) 


Now we can make a key observation: our 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 


Fortunately, the Scalaz library provides a more general, abstract way to implement the composition of functions of the form 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) 


Pay attention to the pointless style that we use here. Functional programmers like to write functions as compositions of other functions, without mentioning arguments.

A Kleisli composition is exactly what we need to implement our 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) 


Note the use of 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) .

Abstracting from xmlnode


Let's summarize our solution and abstract it from XmlNode. We can rewrite it as the following generalized function
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) 

Now we can reuse 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) } 


We wrote a new function, child: JValue => Option[JValue] , to work with JSON instead of XML, but the getByPathGeneric function remained unchanged and works with both XML and JSON.

Abstracting from Option


We can generalize 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) 


Now we can implement our original 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) } 


That way, we can reuse 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 .

In addition, 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) .

So, we can write a function that reuses 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) } 


The original 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.

We have seen how using the Kleisley composition provided by the 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.

Wix Website Designer ,
Mikhail Dagayev

Original article: Wix engineers blog .

Source: https://habr.com/ru/post/304622/


All Articles