0xDEADBEEF

RSS
««« »»»

Čím nahradit Scala.XML

23. 10. 2018

Ve Scale je možné v programu zapisovat přímo XML literály. Ještě pořád, ale už ne příliš dlouho. Martin Odersky (a jeho gang) není jejich fanda, už roky se jich chce zbavit a ve verzi 2.13 se mu to možná konečně podaří.

Technicky vzato XML literály nejsou třeba. Jazyk od verze 2.10 disponuje interpolace stringů, která dokáže tuto funkcionalitu beze zbytku nahradit. S ní bychom psali místo

val t = <tag>body</tag>

o něco málo delší

val t = xml"<tag>body</tag>"

Když padne vestavěné XML, parser se zjednoduší, XML v něm nebude mít výsadní postavení a nebude vázané na neohrabané API scala.xml. Půjde jen o další z mnoha uživatelských knihoven. Z těchto důvodů bychom s ním neměli dlouhodobě počítat. Čím ho tedy můžeme nahradit?

XML literály jsou často používané na generování RSS, Atomu a jiných XML výstupů. Vytvoří se DOM, pak je okamžitě přeměněn na řetězec a zahozen.

(<rss version="2.0">
<channel>
  <title>{blog.title}</title>{
    articles.map(a => <item>
      <title>{a.title}</title>
      <guid isPermaLink="true">{urlOf(a)}</guid>
      <pubDate>{rssDate(a.date)}</pubDate>
      <description>{mkBody(a)}</description>
    </item>)
  }</channel>
</rss>).toString

To vypadá přehledně, ale má to jeden nedostatek: Postaví se kompletní DOM se všemi alokacemi jen proto, aby byl okamžitě zahozen. V případě generování výstupů bude stačit něco jednoduššího. Nepotřebuji reprezentaci XML stromu, nepotřebuji ho zkoumat a měnit, stačí jen přímo vypsat jeho obsah. Pokud možno přehledně.

Tento účel splní starý dobrý XMLStreamWriter zabalený do přehledného API. Vytvoření stejného RSS dokumentu pak může vypadat nějak takhle:

XMLSW.document { w =>
  w.element("rss", Seq("version" -> "2.0")) { w =>
    w.element("channel") { w =>
      w.element("title", blog.title)
      for (a <- articles) {
        w.element("item") { w =>
          w.element("title", a.title)
          w.element("guid", Seq(("isPermaLink", "true")), urlOf(a))
          w.element("pubDate", rssDate(a.date))
          w.element("description", makeBody(a))
        }
      }
    }
  }
}

Není třeba volat writeStartElement a pak odpovídající writeEndElement, tak aby se nekřížily, lexikální blok se o postará o uzavírání tagů a jejich vnořování. Výsledek je měřitelně rychlejší než generování pomocí scala.xml, ale ukázalo se, že čísla jsou nestálá a někdy divoce skáčou.

Příčinu odhalí pohled do zdrojáků. Z nich je vidět, že XMLStreamWriter je (jako mnoho javovských API) určený pro znovupoužití, ne jako jednoduchý nástroj, který se použije jednou a pak je zahozen. Interní implementace alokuje mnoho objektů, některé z nich nutné pro správné zacházení s komplikacemi XML (jmenné prostory), jiné nutné pro správné fungování rozhraní (zásobník otevřených tagů) a další zcela zbytečné (neptejte, hrůzy, na které jsem narazil, mě do dneška budí ze spaní).

Když ale používám zjednodušené API, které samo uzavírá tagy, můžu pod ním podtrhnout ubrus a nahradit XMLStreamWriter za vlastní kód. Když vynechám některé záludnosti XML jako jmenné prostory, není to nic komplikovaného. Escapování XML je triviální - stačí si hlídat jen znaky & < > " a ten poslední jen v některých případech. Dohromady pod 80 řádků kódu, který generuje XML za letu, nealokuje skoro nic a je rychlejší než XMLStreamWriter.


+1: Pro extrakci dat z XML je stále nejšikovnější starý dobrý XPath. Kdysi jsem napsal malou knihovnu xpath.scala, která XPath dodá trochu typovosti.

píše k47 (@kaja47)