elm-xml-decodeつくった

# 背景

以前[Elmでサーバサイドで動くワーカープログラムを作る][worker]を書いたときに触れましたが、XML APIを相手にしたElmアプリを書きたくて、どんなパッケージがあるか調べました。すると、

[worker]: https://qiita.com/ymtszw/items/d195c098445476527539

- [eeue56/elm-xml](http://package.elm-lang.org/packages/eeue56/elm-xml/latest)
    - XMLのParseはできる。が、[けっこう重要なバグがあり、しばらく放置されている](https://github.com/eeue56/elm-xml/issues/3)。
    - Elmの型の値へのdecodeもできるようになっているが、あまりcomposableでないAPIになってる。
- [jinjor/elm-xml-parser][exp]
[exp]: http://package.elm-lang.org/packages/jinjor/elm-xml-parser/latest/
    - @jinjor さんのやつ。Parseはできるが、decoderはない。
- [billstclair/elm-xml-extra](http://package.elm-lang.org/packages/billstclair/elm-xml-extra/latest)
    - あとから存在に気づいたやつ。elm-xmlのAPIでXML => `Json.Value`と変換し、coreの`Json.Decode`を使うというアプローチ。
    - 結果として`Decoder`によるcomposableなAPIを使えるが、ちょっと迂遠に思える。

Elmアドカレの9日めにこんな記事：

- [[Elm] Decoder a からいろいろ理解ってしまおう][dec]
[dec]: https://qiita.com/ymtszw/items/1cabbdbda4273b4c1978

を書いたことからわかるように、このような目的では`Decoder`を単位とするcomposableなAPIが大変便利だと思っていますので、それを実現したかった。

また、[XPath][xp]のように、標的とするXML要素までのパスを指定して掘り出すようなAPIを導入しているものが見当たらず、複雑な構造のXMLを相手にするのが面倒になりそうな印象を受けていました。

[xp]: https://www.wikiwand.com/ja/XML_Path_Language

ということで[ymtszw/elm-xml-decode][exd]を作りました。

[exd]: http://package.elm-lang.org/packages/ymtszw/elm-xml-decode/latest

# elm-xml-decode

READMEにも書いてますが、

> - Provides Decoder-based APIs, sharing the spirit of Json.Decode
> - Also provides DSL-styled decoder compositions, as in Xml.Decode.Pipeline, Xml.Decode.Extra
>     - They share the sprits of Json.Decode.Pipeline and Json.Decode.Extra, respectively
> - Handles list of XML node with identical tags, using ListDecoder type
> - Locates targeting XML nodes using "path" of tags, partially mimicking XPath

この辺をテーマとしました。＋＠で、いい感じのエラーを出力できるような工夫も少ししています。

# 工夫紹介

## `Decoder`-based APIs

[`Decoder`記事][dec]で散々書きまくったので、`Decoder`の良さ、面白さについてはそちらをご覧ください。
elm-xml-decodeでは[jinjor/elm-xml-parser][exp]をParserとして利用しているので、相手にするデータ構造（XMLノードの型表現）はこちらです。

```elm
type Node
    = Element String (List Attribute) (List Node)
    | Text String


type alias Attribute =
    { name : String, value : String }
```

すると、今回用意したい`Xml.Decode.Decoder a`は以下のような型になります。

```elm
type alias Decoder a =
    Node -> Result Error a
```

あとで紹介しますが、ある程度細かくエラーを分類できるように、独自の`Error`型を設けています。

はっきり言って、このように**型定義できた時点で基本的な部分は完成**だと思ってます。あとはパズルみたいなもんです。`string`の例を挙げると、

```elm
{-| Decodes an `XmlParser.Node` into `String`.

  - If the node is `XmlParser.Text`, extracts its value.
  - If the node is `XmlParser.Element` AND contains a single `XmlParser.Text` child,
    extracts its value.
  - Otherwise fails.
  - 
If you want to extract values from node attribute, use [`stringAttr`](#stringAttr) and variants.

[xpn]: http://package.elm-lang.org/packages/jinjor/elm-xml-parser/latest/XmlParser#Node

    run string "<root>string</root>"
    --> Ok "string"
    run string "<root><nested>string</nested></root>"
    --> Err "The node is not a simple text node. At: /, Node: Element \"root\" [] ([Element \"nested\" [] ([Text \"string\"])])"

-}
string : Decoder String
string node =
    case node of
        Text str ->
            Ok str

        Element _ _ [ Text str ] ->
            Ok str

        _ ->
            Err <| DetailedError [] node (Unparsable "The node is not a simple text node.")
```

こんな感じです。

対象とするノードが、

- `Text`ノードであるか、
- 単一の`Text`ノードを子として持つ`Element`ノードである、

どちらかの場合であれば成功させているところがちょっと工夫＋＠です。XPathでは、テキストノードの値（CDATA）を取り出す場合（つまりノードの中の値に興味があるのであって、ノードそのものを必要としているわけではない場合）はpath末尾に`text()`と指定することになりますが、今回のようにAPIレスポンスの中のデータを扱うのが主なユースケースであればだいたいはこのケースに該当するはずなので、`text`のような`Decoder`を置くのは冗長だと思ったのが理由です。

（HTMLスクレイピングのような用途ではまた話は別になると思います。が、今回はXMLをターゲットとする都合上、扱えてもXHTMLだけなので、そのような用途であれば別パッケージになるかと思います）

テキストではなくattributeに興味がある場合は`stringAttr`などのattribute用のdecoderを用意してあります。

また、attributeとテキスト両方を同時に扱いたいような複雑なノードについては、自由に`Decoder`を作ってもらう想定です。それができるのが`Decoder`の良いところです。

`Decoder`-based APIであるということは、自然にApplicativeスタイルな記述もできることになります。`Xml.Decode.Pipeline`および`Xml.Decode.Extra.(|:)`を同梱しました。

## List of XML nodes and `ListDecoder`

これがXMLとJSONの違いが現れるところで、だいぶ悩みました。APIに関するコメント歓迎です。

XMLでは、文書そのものだけを読んだ場合、**「あるノードがただひとつしかその階層に存在しないものなのか、あるいは複数ありうるものなのか」がわかりません**。

```xml:singular
<root>
  <tag>value</tag>
</root>
```

```xml:list
<root>
  <tag>value</tag>
  <tag>value</tag>
</root>
```

上記はいずれもvalidですが、前者では「`<tag>`は複数ありうるが今回はたまたま1つしかない」のか「そもそも`<tag>`は1つしかないのが正しい」のかが不明です。一方JSONでは、

```json:singular
{"tag":"value"}
```

となっていれば`"tag"`は単一の値を持つフィールドで、

```json:list
{"tag":["value"]}
```

となっていれば、`"tag"`は（今回はたまたま1つしか要素がないが）複数の値を配列で持ちうるフィールドであることが明白です。

（リクエストbodyとしては、後者のJSONのようなケースを前者のように略記できるようなAPIもあります。Elasticsearchとかそうですね。でも、レスポンスBodyの方ではあえてそのように非正規化された形式にすることはまずないと思われます。まともな人が作るAPIなら）

ということで、tagやpathを指定できるようなXMLの`Decoder`を作るにあたっては、**使う側が目的の値が単一のものか複数あるものかを指定する**APIになるだろう、という直観がありました。ちなみにこれはelm-xmlなどでも同じです（[`tag`と`tags`がある](http://package.elm-lang.org/packages/eeue56/elm-xml/2.2.3/Xml-Query)。ただし、APIが対称になっておらず、あまりcomposableでないと思う）。

私が選んだのは、**`Xml.Decode.ListDecoder a`というもう一つの型を用意する方式**でした。これは後で紹介する`path : List String -> ListDecoder a -> Decoder a`と組み合わせて使います。例としては以下のような使い方になります。

```elm
someRecordDecoder : Decoder SomeRecord
someRecordDecoder =
    map2 SomeRecord
        (path [ "string", "value" ] (single string))
        (path [ "int", "values" ] (list int))
```

`path`関数は**pathにマッチするNodeを必ずリストで処理する**という一貫性のある関数にしています。`single`と`list`はプリミティブな`Decoder a`から`ListDecoder a`を生成できる関数で、

- `single : Decoder a -> ListDecoder a`はpathにマッチするNodeがただひとつあることを期待し、存在しなかったり、複数あったりしたらエラー
    - decodeした結果としての値も`a`のまま
- `list : Decoder a -> ListDecoder (List a)`はpathにマッチするNodeが0個以上あることを期待する。マッチしたNodeの中にdecodeできないものがあればエラー
    - 結果は`List a`
- `leakyList`は`list`の亜種で、マッチしたNodeの中にdecodeできないものがあれば読み捨て、失敗はしない

となっています。そこそこ統一的な書き方ができ、読みやすいし誤解が少なくなっていると自分では思います。

## なんちゃってXPath

すでに紹介した`path : List String -> List Decoder a -> Decoder a`で、階層の深いXMLも簡単に扱えます。

```elm
exampleDecoder : Decoder ( String, List Int )
exampleDecoder =
    map2 (,)
        (path [ "string", "value" ] (single string))
        (path [ "int", "values" ] (list int))


run exampleDecoder
    """
    <root>
        <string>
            <value>SomeString</value>
        </string>
        <int>
            <values>1</values>
            <values>2</values>
        </int>
    </root>
    """
--> Ok ( "SomeString", [ 1, 2 ] )
```

- 実装は`List.filter`をナイーブに使ったものなので、あまり速くないと思います。良いアルゴリズムがあったらぜひ教えてください
- 幅優先探索になっています
- root階層のtag名は省略できるようになっています
    - と言うか意図的に無視しています。正規のXMLであればただひとつのrootノードがあることは確定しているため
    - 一応、rootノードが想定したものと違うならばエラーとすべきではあるので、`run`(`decodeString`)や`decodeXml`の引数として指定できるようにすべきかも
- 少し弱点があって、**指定したpathの末端まで到達しなかった場合（pathの途中で探索対象が尽きた場合）も空リストを`ListDecoder`に渡すことになり、失敗はしません**。
    - ユースケースが出てきたら、pathの末端以外で探索対象が尽きた場合はエラーにするvariantを用意しようかと思っています。

`path`は指定されたpathを毎回最初から探索するので、同じようなpathにある複数の値を取ってきたい場合は重複する探索が複数回発生して非効率です。が、`Decoder`の特長を活かせばこの問題は容易に解決できます。

```elm
deepDecoder : Decoder ( String, List Int )
deepDecoder =
    map2 (,)
        (path [ "long", "way", "to", "string", "value" ] (single string))
        (path [ "long", "way", "to", "int", "values" ] (list int))
```

上記の例では`"long", "way", "to"`までダブっていますからもったいないですね。以下のようにできます。

```elm
deepDecoder2 : Decoder ( String, List Int )
deepDecoder2 =
    path [ "long", "way", "to"] (single leafDecoder)

leafDecoder : Decoder ( String, List Int )
leafDecoder =
    map2 (,)
        (path [ "string", "value" ] (single string))
        (path [ "int", "values" ] (list int))
```

ヒャッホウ。`Decoder`さまさまだぜ。[こちらで試せます。](https://ellie-app.com/jNRQhHH7ra1/0)

XPathをもう少し真面目に再現しようと思うなら、途中のpathを省略できるようにする（`//`記法）などを考える必要がありますが、可読性を下げる、曖昧さを導入する（、実装が面倒になる）などマイナス面も結構あると思うのであまり気は進みません。`text()`や`@attr`記法などは`Decoder`で指定させるものとしているのでまあなくていいかなと思っています。

## エラー表現

`path`を使った場合など、可能な場合は指定したpathのどの位置でエラーが起きたか、その時の最近傍のノードはどのようなものだったかを返すようにしています。

```elm
type Error
    = SimpleError Problem
    | DetailedError (List String) Node Problem


type Problem
    = NodeNotFound
    | AttributeNotFound String
    | Duplicate
    | Unparsable String
```

例えば先程の`deepDecoder2`を以下のようなノードの不足しているXMLに対して実行すると、

```elm
run deepDecoder2
    """
    <root><long><way><to>
        <string></string>
        <int><values>1</values></int>
    </to></way></long></root>
    """
--> Err "Node not found. At: /long/way/to/string/value, Node: Element \"to\" [] ([Text \"\\n \",Element \"string\" [] [],Text \"\\n \",Element \"int\" [] ([Element \"values\" [] ([Text \"1\"])]),Text \"\\n \"])"
```

このように、エラー発生地点とその近傍のNodeをダンプします。ただ、この例で分かる通りダンプするNodeはもう少しエラー発生地点に近づけられそうなので、あとで修正しておこうと思います。NodeダンプはElmの`Node`を単に`toString`していますが、`formatNode`的な関数で部分的なXMLとして表示したほうが読みやすそう。ちょっとサボっているところ。

# 終わり

そんなわけで、まだ色々改善点はありますが、`Decoder`で快適にXMLを解釈できるという大枠の目的は達成できていると思うので、作ってよかったと思っています。ぜひ使ってみていただけると幸いです。感想や~~文句~~バグレポート、Starなどお待ちしています。

- Elm package: [ymtszw/elm-xml-decode][exd]
- GitHub: [ymtszw/elm-xml-decode](https://github.com/ymtszw/elm-xml-decode)

また、XML APIを相手にしたい場合に、elm-lang/httpの[`Http.get`][hg]と同じように書けると楽そうです。それもおまけで作っておきました。

- Elm package: [ymtszw/elm-http-xml][ehx]

```elm
import Http.Xml
import Xml.Decode exposing (..)

type alias Example =
--  (略)

exampleDecoder : Decoder Example
exampleDecoder =
--  (略)

Http.Xml.get "http://example.com/xml" exampleDecoder
--> (略) : Http.Request Example
```

どちらもよろしくお願いします。

[hg]: http://package.elm-lang.org/packages/elm-lang/http/latest/Http#get
[ehx]: http://package.elm-lang.org/packages/ymtszw/elm-http-xml/1.0.0/Http-Xml