Today, however, I am eating my words. I've just written a small utility that uses Scala to generate the HTML of a simple web page, so I used Scala's native XML and it is Good. It's remarkably simple and intuitive, so I thought I'd share the love.
XML Literal Coercion
So, Scala essentially has XML literals, which means that you can just start typing XML in the middle of your code and the Scala compiler will automatically coerce it into an object that can be used for performing xml-ish operations. If you try this using the 'scala' interpreter, you can see some of what happens under the hood:
scala> val myXml = <test><tag/></test>
myXml: scala.xml.Elem = <test><tag></tag></test>
Note that there's no quotes or special charters around the XML - it's just XML. Scala automatically turns it into an Elem object.
But this isn't where the fun ends! The power of native XML comes from the fact that it's very easy to escape out of XML and into Scala code, right in the middle of your XML. You can escape into Scala simply to include some computed output in the XML, or you can do more crazy things like looping through a list and yielding a list of more Elem objects to be included.
Escaping to Scala: Text
From what I've seen so far, there are two slightly different ways to escape into Scala from an XML literal. The first is used when you want to escape into Scala to include text or another XML element and it looks like this:
val name = "Graham"
val xmlOne = <test>{name}</test>
println(xmlOne)
Output:
<test>Graham</test>
Escaping to Scala: Attributes
If, however, you want to escape at the XML attribute level, you would be WRONG if you tried to do this (like I did):
val xmlTwo = <test name="{name}"/>
println(xmlTwo)
You don't get a compilation error, but the "escaping" is not interpreted, so you get this:
<test name="{name}"></test>
The syntax for escaping an attribute properly looks like this:
val xmlThree = <test name={name}/>
println(xmlThree)
All we've done is drop the quotes, and now we get the output we want:
<test name="Graham"></test>
Escaping to Scala: Elements and Looping
Finally - this is where the power becomes really evident - you can escape into Scala and evaluate an expression that yields a list of Elems in order to include an iteration of values in your XML. Observe:
val hobbies = List("Scala", "Photography", "Cycling")
val xmlFour =
<test name={name}>
{for (hobby <- hobbies) yield <hobby>{hobby}</hobby>}
</test>
println(xmlFour)
Output:
<test name="Graham">
<hobby>Scala</hobby><hobby>Photography</hobby><hobby>Cycling</hobby>
</test>
Did you notice the really cool part of that? After we escaped out of the XML literal into Scala so that we could loop through the list, we then started another XML literal and then escaped out of that one to print the value 'hobby'! At that point we are nested 4-deep: Our outer XML literal contains nested Scala, which contains a nested XML literal, which contains more nested Scala. And yes, this could go on forever, down and down and down (just like the turtles). The great thing is that the syntax is so intuitive that you probably hardly noticed that we had gone that deep in this example - the code is simple and clear.
A Little Warning
Note that, when you're looping and yielding, there's nothing to stop you returning something that's not an Elem - Scala will simply call toString on whatever you return and insert it into your XML as text. For example, the following compiles and runs, but doesn't produce what we want:
object Test {
case class Scala;
case class Photography;
case class Cycling;
def main(args: Array[String]) {
val name = "Graham"
val xmlFive =
<test name={name}>
{for (hobby <- List(Scala, Photography, Cycling)) yield hobby}
</test>
println(xmlFive)
}
}
The output from this?
<test name="Graham">
<function><function><function>
</test>
Ewwww… (and, yes, the entities do appear in the output)
In conclusion: I used to think that native XML in Java was a stupid idea. Now I don't really care because Scala has it and it looks good and it works fine so whenever I need to produce XML from within code I'll just use Scala.
A Side-Note
If you're slightly genius, you're probably wondering why, in the last example, I showed my whole 'Test' object, whereas in all the other examples I just showed the contents of the main() method. The simple explanation is that I had originally defined the case classes in the main method, but I got this compile error:
Test.scala:19: error: forward reference extends over definition of value xmlFive
{for (hobby <- List(Scala, Photography, Cycling)) yield hobby}
^
To be honest, I wasn't (and I'm still not) exactly sure why this was a problem, but it seemed obvious that scalac was not happy for me to define a case class inside a method and then use that class in the same method (well, actually, I'm using its companion object). I wonder if the compiler injects the instantiation of the companion object at the end of the method, rendering it unusable throughout? That would seem a bit silly. Anyway, the obvious fix was to move the declaration of the case classes out of the definition of main(), and hence why I posted the whole class as the example.
Thanks for the article, Grazer. That helped me to understand what I was missing. Now I'm off to search for a way to get Scala to make the closing tag as />, instead of showing the closing tag separately.
ReplyDeleteNo problems, Daniel.
ReplyDeleteIf you find out how to do empty tags, I'd love to know the answer!
Cheers,
Graham.