Mastodon

メタデータ API 緩衝層を作ったことで Go の非関数型言語性を知った

動機

私が利用している国立国会図書館サーチのメタデータは、国立国会図書館ダブリンコアメタデータ記述(DC-NDL)に準拠している。 今回は書籍のページ数の情報が必要になったため、DC-NDL の該当するフィールドを調べてみることにした。

ndlsearch.ndl.go.jp

<dcterms:extent>120p ; 22cm</dcterms:extent>

ページ数は dcterms:extent にリテラルとして収められているらしい。

API 緩衝層

一般に国立国会図書館サーチのような API を挟む場合、バックエンド(またはフロントエンド)と API エンドポイントとの間に、自分のアプリケーションで用いるために API から返されるデータを加工する緩衝層が必要になる。

今回の場合は titlecreatordescriptionpublisher 、そして pages のデータが必要であり、以下の for 文で構造体に情報を代入している(プログラム言語は Go 言語)。 title 等の pages 以外のフィールドの値は加工なしで用いることができるが、pages の値だけは dcterms:extent から抽出する必要がある。 つまり、 title 等の値に対する変換は恒等写像である。

for _, prop := range bibPropKeys {
    value := bibProp(prop)
    switch prop {
    case "extent":
        re := regexp.MustCompile(`\d+`).FindString(value)
        i, err := strconv.ParseInt(re, 10, 64)
        if err != nil {
            return nil, err
        }
        bibliographicInfo.Pages = int(i)
    case "title":
        bibliographicInfo.Title = value
    case "creator":
        bibliographicInfo.Creator = value
    case "description":
        bibliographicInfo.Description = value
    case "publisher":
        bibliographicInfo.Publisher = value
    }
}

ただし bibPropKeys は文字列 title 等が入っている配列、bibliographicInfo は構造体である。

しかし私はこれを手続き的であり、データと処理が密結合であると思う1。 そのため、この switch 文を 「propKey」と「変換を行う関数」からなる対(map)に抽出したい。

しかしそれは Go だと難しい。 関数が第一級関数とはいえ、pages 以外のフィールドの値は String 型である一方、pages の値だけは Int 型であり、Go には合併型が存在しないため、map の型は map[string]func(string) interface{} となってしまう。つまり、型が曖昧になってしまう。

関数型言語である Elixir であれば、関数 Function.identity/1 を用いて次のように変換写像を明瞭に分離できる。

hexdocs.pm

conversion_map = %{
  "extent" => fn value ->
    re = Regex.run(~r/\d+/, value) |> List.first()
    case Integer.parse(re) do
      {i, _} ->
        %{pages: i}
      _error ->
        {:error, "Failed to parse integer for extent"}
    end
  end,
  "title" => Function.identity(),
  "creator" => Function.identity(),
  "description" => Function.identity(),
  "publisher" => Function.identity()
}

Enum.reduce(bib_prop_keys, %{}, fn prop, acc ->
  conversion_fn = Map.get(conversion_map, prop, fn _ -> fn _ -> %{} end end)
  value = bib_prop(prop)
  updated_info = conversion_fn.(value)
  Map.merge(acc, updated_info)
end)

つまり、API 緩衝層で行う変換の取捨選択を Map で明瞭に表現することができるのである。 加工せずとも使えるフィールドは identity/1 として表現し、加工が必要なフィールドには適宜適切な変換関数を与える。 このようにして、API 緩衝層が行うべき「変換」を手続き的ではなく、(空間的な)「データとして」与えるのである。

まとめ

今回の件を通して、Go は純粋関数型言語ではなく、関数型プログラミングには使いにくい言語であることが分かった。


  1. 関数型プログラミング信者であるため。