Некоторое время мы поддерживали приложение, которое обрабатывает данные XML и JSON. Обычно обслуживание состоит из исправления дефектов и добавления незначительных функций, но иногда это требует рефакторинга старого кода.
Рассмотрим, например, функцию, которая извлекает узел XML по пути:
01
02
03
04
05
06
07
08
09
10
11
|
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 для соответствия новым требованиям.
Рефакторинг с Клейсли Композицией
Давайте выделим фрагмент кода, который создает функцию для извлечения дочернего узла по имени. Мы можем назвать его createFunctionToExtractChildNodeByName , но давайте для краткости назовем его child.
1
2
|
val child: String => XmlNode => Option[XmlNode] = name => node => node.child.find(_.label == name) |
Теперь мы можем сделать ключевое наблюдение: наш getByPath является последовательной композицией функций, которые извлекают дочерние узлы. Код ниже демонстрирует реализацию этой композиции:
1
2
3
4
5
6
|
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] , которая имеет метод> => для цепочки оболочек Kleisli так же, как andThen цепочки регулярных функций. Мы будем называть эту цепочку составом Клейсли. Код ниже предоставляет пример композиции:
1
2
3
4
5
6
7
|
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 как композиции функций, извлекающих дочерние узлы.
1
2
3
4
5
6
|
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] в качестве нейтрального элемента сгиба . Нам нужен этот нейтральный элемент для обработки особого случая, когда path равен Nil . Kleisli.ask [Option, XmlNode] — это просто псевдоним функции из любого узла в Some (узел) .
Абстрагирование над XmlNode
Давайте обобщим наше решение и абстрагируем его через XmlNode . Мы можем переписать его как следующую обобщенную функцию:
1
2
3
4
5
|
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 ):
01
02
03
04
05
06
07
08
09
10
|
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 еще дальше и абстрагировать его от Option с помощью Scalaz, который предоставляет экземпляр scalaz.Monad [Option] . Таким образом, мы можем переписать getByPathGeneric следующим образом:
1
2
3
4
5
6
7
|
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 с getByPathGeneric :
1
2
3
4
5
|
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. \ / (Он же дизъюнкция ), которая является монадической версией с правым смещением scala.Either . Кроме того, Scalaz предоставляет неявный класс OptionOps с методом toRightDisjunction [B] (b: B) , который преобразует Option [A] в scalaz.B \ / A, так что Some (a) становится Right (a), а None становится Left (б) Вы можете найти больше информации о \ / в других блогах .
Таким образом, мы можем написать функцию, которая повторно использует getByPathGeneric , чтобы возвращать сообщение об ошибке вместо None, если узел не найден:
1
2
3
4
5
6
7
|
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 .
Мы видели, как использование композиции Kleisli, предоставляемой Scalaz, может исключить универсальную функцию getByPathGeneric , которую мы в дальнейшем абстрагировали с использованием обобщений (для поддержки JSON) и дизъюнкции (чтобы обобщить через Option ).
Ссылка: | Рефакторинг с Kleisli Composition от нашего партнера JCG Михаила Дагаева в блоге Wix IO . |