System.Xml.Linq provides a nice, functional way of querying XML however with F# operator overloading we can create a nice mini DSL to have even cleaner code for XML querying.
With this DSL we can mimic XPath and XQuery quite closely.
Consider the following simple XML:
<Vehicle xmlns="urn:auto/v1"> <Engine> <Part number="abc" type="piston" cost="4.0"/> <Part number="def" type="camshaft" cost="25.0" /> </Engine> <Body> <Part number="234" type="fender" cost="32.47" /> <Part number="423" type="door" cost="45.0" /> </Body> </Vehicle>
Say we want to find just the engine parts whose cost is greater than $5.00 then a typical XQuery would be:
xquery version "1.0"; declare namespace auto="urn:auto/v1"; for $p in /auto:Vehicle /auto:Engine /auto:Part where $p/@cost > 5.0 return $p
Now with some simple definitions, we can write F# code that mimics the above XQuery reasonably closely.
First we define three operators (“/-“ , “/+”, “@”) and then we define a simple function “auto” to give back a fully namespace qualified name. Finally we use list comprehension to perform the actual query:
let doc = XDocument.Load(file) let inline (/-) (x : XContainer) n = x.Elements n let inline (/+) (xs : XElement seq) n = xs |> Seq.collect(fun x -> x /- n) let inline (@) (x:XElement) n = x.Attribute(XName.Get(n,"")).Value let auto x = XName.Get(x,"urn:auto/v1") [for p in doc /- auto"Vehicle" /+ auto"Engine" /+ auto"Part" do if p @ "cost" |> float > 5.0 then yield p]
Once the operators are defined, the rest of the code is very clean and quite similar to the XQuery code.
Note another useful operation would be (“/*”) to get all the descendents of a node as in:
let inline (/*) (x : XContainer) n = x.Descendants n
Over the course of doing some work with actual XML data, I found the following helper functions to be useful:
let first xs = if Seq.isEmpty xs then None else Some(Seq.nth 0 xs) let localName (x:XElement) = x.Name.LocalName let value = function None->None | Some (y:XElement) -> Some(y.Value)
The “first” function can be used to get the first element of a sequence. It can be composed with any of “/…” operators defined above and with the “value” function. For example:
someNode /- ns"Child1" |> first |> value