Search code examples
haskellxml-parsinghaskell-lens

xml-lens API surprise: function composition and travelsals


How are following expressions different in their intent? I'm surprised that they actually type check and produce different results in example below.

(./) ::
  Plated a =>
  Traversal s t a a -> Traversal a a u v -> Traversal s t u v
        -- Defined in âText.XML.Lensâ
infixr 9 ./

and

(.) :: (b -> c) -> (a -> b) -> a -> c   -- Defined in âGHC.Baseâ
infixr 9 .

And how they work with typical pom.xml file:

*Main> x ^.. root ./ ell "version" . text
["1.0-SNAPSHOT"]
*Main> x ^.. root ./ ell "version" ./ text
[]

where

text ::
  Control.Applicative.Applicative f =>
  (Data.Text.Internal.Text -> f Data.Text.Internal.Text)
  -> Element -> f Element
        -- Defined in âText.XML.Lensâ

and

data Element
  = Element {elementName :: Name,
             elementAttributes :: containers-0.5.5.1:Data.Map.Base.Map
                                    Name Data.Text.Internal.Text,
             elementNodes :: [Node]}
        -- Defined in âText.XMLâ
instance Plated Element -- Defined in âText.XML.Lensâ

Solution

  • (./) is based on a Plated instance, which is the class of types that have "children" of the same type. HTML elements, an AST for mathematical expressions, etc.

    Here (.) just chains traversals. So

    *Main> x ^.. root ./ ell "version" . text
    ["1.0-SNAPSHOT"]
    

    goes to all children of the root node with a local name == "version" and collects the text content inside them. (there's only one such node here)

    (./) however adds an additional step.

    *Main> x ^.. root ./ ell "version" ./ text
    []
    

    This goes to any child of the root node with a local name == "version", then focuses on all the children of these nodes and picks the text content inside them. If no children are found, the resulting traversal most likely doesn't point at any result, which is probably why you get an empty list.

    Basically, l ./ l' traverses what's described by l, resulting in a number of targets, then switches the focus from each of these targets to their "children of the same type" (as described by the Plated instance), before then traversing these with l'.

    Note that you have to use ./ after root because you want to descend into all the immediate children of the root node, picking up only the ones with the name you're interested into. But when looking at the content of the "version" element(s), you just want the immediate content inside them, no need to traverse children elements further, hence the necessity to use (.) instead of (./).