jq.el というライブラリを作って学んだこと

この記事は [Emacs Advent Calendar 2020](https://qiita.com/advent-calendar/2020/emacs) の 8 日目の記事です。

---

こんにちは。

普段は {Python,Ruby,Vue,React} あたりで Web アプリケーションを書いたり、機械学習などをしている Emacser です。
趣味は日本語で、好きな言語学者は高橋太郎先生や三浦つとむ先生です。

今回はちょっと前に作った [jq.el](https://github.com/p-baleine/jq.el) について書いてみます。

僕個人は Advent Calendar で何か作ってみた系の記事は正直興醒めしてしまうタチなのですが、他にネタを思いつきませんでした、申し訳ありません :bow_tone2:

せっかくなので jq.el を作って公開した上で得た学びについても書きたいと思います。

# [jq.el](https://github.com/p-baleine/jq.el) について

名前からお気付きの方も多いと思いますが jq.el は CLI 向け JSON 処理系の一つである  [jq](https://stedolan.github.io/jq/) の Emacs Lisp binding です。

例えば以下のような JSON 文字列があるときに、

```elisp
(setq input "
[
  {
    \"name\": \"Ness\",
    \"age\": 12,
    \"origin\": { \"country\": \"Eagleland\", \"town\": \"Onett\" }
  },
  {
    \"name\": \"Paula\",
    \"age\": 11,
    \"origin\": { \"country\": \"Eagleland\", \"town\": \"Twoson\" }
  }
]
")
````

以下のように JSON 文字列に対する処理を emacs lisp で書けます:

```elisp
(cl-loop for x iter-by (jq input ".[] | .origin.town") collect x)
;; => ("Onett" "Twoson")
```

# 動機

jq.el を書いたのは今年の 9 月頃で、この頃業務で [Redash](https://redash.io/) を利用することが多く、Redash の Emacs 向けクライアントを作りたいと思ったのがきっかけでした。
Redash は勿論、リッチなクエリエディタを提供してくれてはいるのですが、当然 Emacs キーバインドは使えず、またプログラマブルでもないためその煩わしさを解消したく Emacs 向けのクライアントを実装できないかと模索していました。
(結局 Redash の Emacs 向けクライアントについては面倒になって作っていませんが…)

僕は Emacs Lisp や Redash に限らず何かしらの言語向けの API クライアントを作りたいとき、まず shell 上で curl や jq を用いて API と仲良くなろうとします。これが API クライアントを作成するときのベストなアプローチかは議論の余地がありますが、結果として手元にいくつかの jq のスニペットが出来あがります。

Node.js でも Ruby でも Python でも便利な json 向けのライブラリが組込まれていて、前述の jq のスニペットは容易に翻訳可能ですが、これが Emacs Lisp となると途端に直感的に翻訳することが難しくなります。
Emacs は [json.el](https://github.com/emacs-mirror/emacs/blob/master/lisp/json.el) という json 文字列処理に関連したライブラリを内包してくれていますが、ここで提供される json-read-from-string の返却値は連想配列であり、たかだか所望のフィールドにアクセスする程度の操作でも、そのコードは煩雑になりがちです。
先程の例を json-read-from-string を使って実装すると以下のようになるかと思います:

```elisp
(let (parsed (json-read-from-string input))
  (cl-loop for entry across parsed
    collect (let* ((origin (cdr (assoc 'origin entry)))
                   (town (cdr (assoc 'town origin))))
              town)))
;; => ("Onett" "Twoson")
```

Emacs Lisp 向けの API クライアントを作るとき、なぜ jq のように簡単に JSON 文字列を操作できないのか？
(そして API と仲良くなる過程で手に入る jq のスニペットをそのまま利用できないか？)

これが jq.el を書いた動機です。

# 仕組み

jq は C 言語向けの API として [libjq](https://github.com/stedolan/jq/wiki/C-API:-libjq) を提供してくれています。
また、Emacs 25.1 以降では共有ライブラリをロードする [Dynamic Modules](https://www.gnu.org/software/emacs/manual/html_node/elisp/Dynamic-Modules.html) という機能が提供されています。
そのため Emacs の Dynamic Modules の規約に従って libjq を Emacs Lisp の世界に提供する .so (.dll) ファイルを作成すれば、簡単に libjq の機能を Emacs Lisp の世界で利用できるようになります。

jq.el では上述の作戦に則って C++ で実装しています ([ここら辺](https://github.com/p-baleine/jq.el/blob/master/src/jq_impl.cc))。

実装にあたっては以下の Python や Ruby 向けの binding ライブラリを参考にさせて頂きました:

-   <https://github.com/mwilliamson/jq.py>
-   <https://github.com/winebarrel/ruby-jq>

# jq.el を作ってみた上での学び

## reddit におけるコメントに対して

今回、せっかく jq.el を作ってみたので reddit に投稿してみました。

<https://www.reddit.com/r/emacs/comments/imf6wd/new_package_jqel/>

肯定的なコメントも頂けましたが、それ以外のコメントに対し思ったことについて。

### Emacs Lisp 向けライブラリの書き方について

> Your package have 0 Emacs packages dependencies, so you don&rsquo;t need Cask
> ;; Homepage: https://github.com/p-baleine/jq, the URL is wrong
> ;; Package-Requires: ((emacs "27.0.60") (cl-lib "0.5")), you don't need cl-lib, it is builtin since Emacs 24.3, and the latest Emacs is 27.1 and generator.el is added in Emacs 25.1, you can probably lower your Emacs version requirement.
> https://www.reddit.com/r/emacs/comments/imf6wd/new_package_jqel/g44r58e?utm_source=share&utm_medium=web2x&context=3

外部の Emacs パッケージへの依存がないことから Cask は不要であることや、cl-lib は Emacs に内包されていることから、`Package-Requires` への記述が不要であることなどを指摘してもらいました。
また、この方には他にも CI 周りなどについても色々と教えていただきました (https://github.com/p-baleine/jq.el/pull/8) 。

自分の無知を自覚すると共に、こういう指摘は勉強になってとてもありがたいし、こうやって指摘してくれる姿勢を尊敬しています。

### jq というプログラミング言語自体が煩わしい

> The problem with spending a half-hour familiarizing yourself with jq is that you have to do it over and over again. The syntax sucks so bad that I, for one, forget it no more than a week after learning it.
> https://www.reddit.com/r/emacs/comments/imf6wd/new_package_jqel/g3zv2nw?utm_source=share&utm_medium=web2x&context=3

これは正直おっしゃる通りだと思います。僕も何かやりたいとき毎回 jq のドキュメントを参照しています。後、このコメントへの返信の &ldquo;Sounds like emacs! :)&rdquo; は笑いました。

### let-alist とか使えばイイじゃん

> Maybe you aren't aware of tools like let-alist or a-get*, which make such nested retrieval simple.
> https://www.reddit.com/r/emacs/comments/imf6wd/new_package_jqel/g43obxl?utm_source=share&utm_medium=web2x&context=3

これもおっしゃる通りなのですが、「動機」でも書いた通り僕としては単に json の奥深くにアクセスする方法を探しているというよりは、jq のプログラムを実行できること自体の方に魅力を感じ jq.el を作りました。一応 README の方にも動機については記載しているのですが、僕の文章能力や英語力が足りずうまく伝わっていないのだと考えています。

## もらった PR に対して

README に記載の方法ではインストールできなかったとのことで、README を更新する Pull Request を貰いました。
基本的には依存関係を apt などでインストールする手順を追記してくださっているのですが、僕の手元の docker ではこの方の言及する問題を 100% は再現できず、部分的に PR を受け入れる形になりました。またこの方とのやり取りもあまり上手く運べず、結果として以下のコメントをもらって、

> Though I&rsquo;m not sure it&rsquo;s terribly useful in its current state, most folks who want to build a package will have a compiler, autoconf and libtool available. The problem was more finding the right header packages for oniguruma, jq and emacs. If you deem it no longer necessary, feel free to close the PR.

<https://github.com/p-baleine/jq.el/pull/5#issuecomment-688471836>

正直翌日とても落ち込んでいました。

僕のコミュニケーション能力の欠如に起因する問題なのは分かってはいるのですが、こういうことあると作ったもの公開することに躊躇したくなります…。

## 一番の学び

なんだか Advent Calendar に似つかわしくない落ち込んだ内容になってきましたが :sweat_smile:、実はこの件で一番思ったことは、

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">プログラムのライブラリとか作ったら公に晒さないと、そもそも恥をかけないし、その分勉強の機会を失うという、当たり前のことを学びました。誰かに作ったもの見てもらって恥をかくってとても効率の良い勉強法なんだけど、なかなか実践できない…</p>&mdash; p-baleine (@parc_b) <a href="https://twitter.com/parc_b/status/1302519426755952646?ref_src=twsrc%5Etfw">September 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これに尽きます。

前述のもらった PR の件のように落ち込むこともありますが、これも僕のコミュニケーション能力の低さを学ぶ機会であったと考えられるとも思っています。

普段、プログラムにせよブログ記事も Qiita の記事も殆ど公開しないタチなのですが、もう少し積極的になろうと思わなくもない…善処します。