Todd O'Bryan
Todd O'Bryan

Reputation: 2260

Modify XML in Scala without mutation?

I'm trying to replace pieces of XML and need an accumulator along the way. Say I have a fill-in-the-blank question stored as XML like so:

val q = <text>The capitals of Bolivia are <blank/> and <blank/>.</text>

At some point I'm going to want to turn those blanks into HTML input elements and I need to be able to distinguish the first and second so I can check them. (Ignore the fact that, in this case, the two capitals can appear in either order--that's a headache I'll deal with later.)

Thanks to some lovely answers on StackOverflow, I produced the following solution:

import scala.xml._
import scala.xml.transform._

class BlankReplacer extends BasicTransformer {
  var i = 0

  override def transform(n: Node): NodeSeq = n match {
    case <blank/> => {
      i += 1
      <input name={ "blank.%d".format(i) }/>
    }
    case elem: Elem => elem.copy(child=elem.child.flatMap(transform _))
    case _ => n
  }
}

and this works reasonably well. I have to create a new BlankReplacer() each time I want to start re-numbering, but it pretty much works:

scala> new BlankReplacer()(q)
res6: scala.xml.Node = <text>The capitals of Bolivia are <input name="blank.1"></input> and <input name="blank.2"></input>.</text>

Here's the question. Is there an easy way to avoid the mutation I have to do each time I replace a <blank/>? What I have doesn't strike me as horrible, but I think this could be cleaner if I weren't creating a new instance of the BlankReplacer class every time I had to convert a question to HTML. I'm sure there's some way to turn this into an accumulator, but I'm not sure how to do it.

Thanks! Todd

Upvotes: 4

Views: 1123

Answers (2)

Travis Brown
Travis Brown

Reputation: 139028

This is precisely the kind of problem that Anti-XML's zippers are designed to solve:

In other words, we started with an XML tree, we drilled down into that tree using a selector, we derived a new version of that result set with some modifications (in our case, a new attribute), and now we want to go back to the tree we originally had, except with the modifications we made way down in the bowels. This is what a zipper is for...

In your case you could do something like this:

import com.codecommit.antixml._

def replaceBlanks(el: Elem) = {
  var i = 0
  (el \\ "blank").map { _ =>
    i += 1
    <input name={"blank.%d".format(i)}/>.convert
  }.unselect
}

Or you can avoid the var using the trick in this answer:

def replaceBlanks(el: Elem) = {
  val blanks = el \\ "blank"

  (0 until blanks.size).foldLeft(blanks) {
    case (z, i) => z.updated(i, z(i).copy(
      name = "input",
      attrs = Attributes("name" -> "blank.%d".format(i + 1)))
    )
  }.unselect
}

Now we can apply the method to your element (after converting it to an com.codecommit.antixml.Elem):

scala> println(replaceBlanks(q.convert))
<text>The capitals of Bolivia are <input name="blank.1"/> and <input name="blank.2"/>.</text>

The trick is that we can use \\ to dig down into the tree, just as with scala.xml, but unlike scala.xml we can make modifications to the resulting "node set" (actually a zipper) and then put them back into its original context using unselect.

Upvotes: 4

Chris
Chris

Reputation: 2791

Scales Xml provides folding over paths allowing you "modify" a tree and to accumulate...

import scales.utils._
import ScalesUtils._
import scales.xml._
import ScalesXml._

// the xml example
val q = <("text") /( "The capitals of Bolivia are ", <("blank")," and ",<("blank"),".")

// which elems to fold on?
val p = top(q) \\* "blank"

val f = foldPositions(p, p.size){ // size is used as the starting point for the fold
  case (question, path) =>
(question - 1, Replace( <("input") /@ ("name" -> ("blank."+question) )) )
  }

// f is an either, so we assuming its working here, and ._1 is the accumalator, _.2 the Path
val newTree = f.left.get._2.tree

The only quirk is that it accumulates in reverse document order, there is also a non-accumulative version. This allows for combinations of transformations when some are destructive (for example changing a subchild and then deleting it in another transformation all just works).

The input to the fold itself is any Iterable of Paths, as long as they are in the same tree, allowing you to combine queries as you see fit.

See here for more details on how to Fold Xml in Scales

Upvotes: 1

Related Questions