The most interesting part of xml-lens is API for XML transformations. It’s divided into two layers: Optics API (also called low-level API) and DSL (also called high-level API).

Optics API (low-level API)

Purpose of Optics API

Optics API is the heart of xml-lens. Optics offers way of constructing immutable, principled APIs on any ADTs. Therefore, they promise a good solution for operating on XML. circe-optics was also an inspiration for such solution.

The main purpose of Optics API is to provide set of lawful Optics to modify and traverse xml-lens AST. The focus of that API is more on completeness rather than on ease of use.

When to use Optics API

You should use Optics API if high-level API does not provide functionality you need. Otherwise use high-level API which is easier to use.

When utilizing Optics API you will operate directly on Monocle types. There is some theory behind them and therefore some basic knowledge of it is neccessary to comfortably use this kind of API. Understanding what is Lens, Prism and Traversal will be particularly helpful. If you don’t know where to start educating about Optics you may find section resources relevant.

How to use Optics API

In order to explain how to use Optics API it will be presented some motivational example, followed by analysis how it works and suggestions how to explore Optics API by yourself.

Exemplary usage

In this example we will modify all text nodes in path b/c:

import pl.msitko.xml.parsing.XmlParser
import pl.msitko.xml.printing.XmlPrinter
import pl.msitko.xml.optics.ElementOptics._
import pl.msitko.xml.optics.LabeledElementOptics._
import pl.msitko.xml.optics.XmlDocumentOptics._

val exampleXml = """<?xml version="1.0" encoding="UTF-8"?>
<a>
   <b>
      <c>item1</c>
      <d>item2</d>
   </b>
   <b>
      <c>item1</c>
      <e>item2</e>
      <c>item3</c>
   </b>
</a>"""

val parsed = XmlParser.parse(exampleXml).right.get

val traversal = rootLens.composeTraversal(deep("b").composeTraversal(deeper("c")).composeOptional(hasTextOnly))

val res = traversal.modify(_.toUpperCase)(parsed)
XmlPrinter.print(res)
// res4: String =
// <?xml version="1.0" encoding="UTF-8"?>
// <a>
//    <b>
//       <c>ITEM1</c>
//       <d>item2</d>
//    </b>
//    <b>
//       <c>ITEM1</c>
//       <e>item2</e>
//       <c>ITEM3</c>
//    </b>
// </a>

You can find DSL equivalent here. The result is as expected, now let’s analyze API more deeply.

Usually you’ll start constructing any transformation with:

import pl.msitko.xml.optics.XmlDocumentOptics
// import pl.msitko.xml.optics.XmlDocumentOptics

XmlDocumentOptics.rootLens
// res5: monocle.Lens[pl.msitko.xml.entities.XmlDocument,pl.msitko.xml.entities.LabeledElement] = monocle.PLens$$anon$7@118612c7

which as you see is simply monocle.Lens. Then to compose it with other optics you can use its compose... methods.

You man wonder how can you know what to import from import pl.msitko.xml.optics. The general rule is that optics which source is of type A are defined in pl.msitko.xml.optics.A

Example: You started defining definition with rootLens of type Lens[XmlDocument, LabeledElement]. Now you need to compose it with optics which source type is LabeledElement. Then you need to import pl.msitko.xml.optics.LabeledElement.

Unfortunately xml-lens cannot define package object that would define all optics and would let you to include all optics with one import. The reason for that is that some names of optics are the same between different source type, e.g. LabeledElementOptics.children and ElementOptics.children.

DSL (high-level API)

Purpose of DSL

The goal of DSL is to provide a set of convenient combinators for the most common operations. Those combinators are implemented on top of Optics API.

How to use DSL

Firstly you need to import pl.msitko.xml.dsl._. It will bring root into scope. root represents the root element of XML document. Therefore, it serves as starting point for defining any transformations.

Exemplary usage

We will rewrite the same transformation as described here, this time with DSL.

import pl.msitko.xml.parsing.XmlParser
import pl.msitko.xml.printing.XmlPrinter
import pl.msitko.xml.dsl._


val exampleXml = """<?xml version="1.0" encoding="UTF-8"?>
<a>
   <b>
      <c>item1</c>
      <d>item2</d>
   </b>
   <b>
      <c>item1</c>
      <e>item2</e>
      <c>item3</c>
   </b>
</a>"""

val parsed = XmlParser.parse(exampleXml).right.get

val transformation = (root \ "b" \ "c").hasTextOnly.modify(_.toUpperCase)

val res = transformation(parsed)
XmlPrinter.print(res)
// res11: String =
// <?xml version="1.0" encoding="UTF-8"?>
// <a>
//    <b>
//       <c>ITEM1</c>
//       <d>item2</d>
//    </b>
//    <b>
//       <c>ITEM1</c>
//       <e>item2</e>
//       <c>ITEM3</c>
//    </b>
// </a>

Resources on Optics

Articles

Talks

  • Ilan Godik’s talk - great introductory talk into Optics in Scala using Monocle by one of its maintainers. Short and does not require any specific knowledge upfront. Also introduces Van Laarhoven Lenses
  • Julien Truffaut’s talk - Julien is an author of Monocle, in this talk he provides great overview and intuitions about various types of Optics
  • another talk by Julien Truffaut - this one is about JsonPath - concept already mentioned in this article in section covering circe-optics
  • Brian McKenna’s talk - Brian goes through Optics libraries in a few different languages: PureScript, Haskell, Scala and Java. Mentions nice examples of applications including representing web pages as Optics which allows to navigate between state and UI in Halogen, working with Kinesis records in Haskell, handling errors with Prisms in Scala