Markdown in 2016
Markdown、あなたのすぐとなりに潜む問題
昨日は toc 拡張の話ついでに、現在の Sphinx と markdown を取り巻く環境について愚痴ったわけですが、Markdown 業界は 2016年のこの時期になっても、いまだに共通的な仕様が決まっていません。
2004年、John Gruber によって生み出された Markdown は、12歳を迎えた現在、さまざまな markdown 処理系を持っています。
そして、不幸にも実装によって markdown の処理はそれぞれ異なってしまっています。
これは Markdown の仕様が曖昧であることと、それぞれの処理系で文法の拡張を行っていることから来ています。
Markdown 処理系による違い
Markdown の仕様が曖昧であることは、さまざまな混乱を生み出しました。
babelmark2 が示すように、同じマークアップであっても処理系によって出力結果が変わってしまうということが起きています。
これに対して、2014年10月に CommonMark プロジェクトが立ち上がります。
CommonMark プロジェクトは Markdown のあいまいな仕様を整理しなおし、マークアップごとにどういう風にレンダリングされるべきか、また解釈の優先順位はどうなるべきかなど、詳細な仕様を定めることをゴールとしたプロジェクトです。
昨日触れた recommonmark も、この CommonMark に準拠した実装のひとつです。
(より正確には、CommonMark 仕様に準拠した python 実装である CommonMark パッケージを Sphinx から利用できるようにラッピングしたものです)
文法の拡張と方言化
もうひとつの Markdown の問題は文法の違いです。
オリジナルの Markdown には必要最低限のマークアップしか定義されておらず、それ単体では表すら書くことができません。
オリジナルの Markdown では表や脚注、定義リストなど、幾つかの要素を持っていないため、
ドキュメントの内容によっては表現できないものがあります。
Markdown には HTML を含めることができるので、HTML をごりごり書くことで回避はできますが、あまりスマートではないですよね。
こうした課題を解決するために、おおくの Markdown 処理系で文法を拡張しました。
有名なのものとしては、多くの人が使っている GFM こと Github Flavored Markdown や
PHP Markdown Extra などが挙げられます。
他にも拡張したものは数多く存在します(参考: Wikipedia Markdown#利用例と方言)
こうして、Markdown には "基本的な文法” は同じだが、ちょっとしたおまけの文法がある Markdown の方言が雨後の筍のように大量に存在するのです。これが現代のバベル、Markdown なのです*1。
2016年に起きたこと
2016年は Markdown の大きな節目になる…はずでした。
CommonMark 1.0 がリリース…されなかった
commonmark.org には、以前からこんな記述があります。
When is the spec final?
The current version of the CommonMark spec is complete, and quite robust after a year of public feedback … but not quite final.
With your help, we plan to announce a finalized 1.0 spec and test suite in early 2016.
しかし、あと2日弱で 2016年が終わろうとしている今日現在でも、1.0 はリリースされていません。
11月18日には 0.27 がリリースされていますが、いつごろ 1.0 はリリースされるのでしょうか。
CommonMark が定まることで、処理系ごとにマークアップが異なっていた問題は徐々に収束していくものと思われます。
もちろん、これから各処理系を手直しして CommonMark に合わせていく作業は残っていますが、
Markdown の標準化に向けては大きな一歩になるはずです。
(まだ 1.0 とはいえ、かなり形になってきているものなので、各ライブラリでは対応が進んでいるとなお良いのですが…)
Markdown が RFC に登録された。media type として。
以前から Markdown を RFC に登録しようという動きはあったのですが、ついにそれが実を結びました。
RFC7763 The text/markdown Media Type と RFC7764 Guidance on Markdown: Design Philosophies, Stability Strategies, and Select Registrations がそれです。
これまで説明してきたとおり、処理系によって解釈がまちまちだったり、さまざまな方言が存在する状況で、Markdown はどのように標準化されたのでしょうか?
ふたつの RFC では、Markdown に関するパラメータが次のように定義されました。
- IANA で markdown の方言を Markdown variants として登録管理することになった
- media type には text/markdown を使うことになった
- このふたつを組み合わせて Content-Type ヘッダで text/markdown; variant=Original と指定できる
つまり、RFC は Markdown の “文法” を標準化したのではなく、
1. Markdown の文法が分断されていることを追認した上で、
2. どの文法を使っているのか(使うのか)を variant として media type で示すことができる
という標準化だったのです。
わかりますね? IETF でもばらばらになった方言はどうにもならなかったのです。
RFC7763 には次のような説明があります。
Markdown variations (some might say "innovations") are designed to
be broadly compatible with humans ("humane"), but not necessarily
with each other. Therefore, syntax in one Markdown derivative may
be ignored or treated differently in another derivative.
意訳:
まとめ
- Markdown の標準化はちょっとずつ進んでるみたい
- でも、まだ方言問題は解決しそうにない
- もし処理系を作る場合は方言を増やさないようにしましょう
おまけ
CommonMark にテーブル記法を足そうとか、recommonmark (CommonMark 処理系)に様々な便利文法を足そう、みたいな提案やイシューが並んでいるのを見て、まだしばらくは方言問題は解決しないんだろうなあ、と感じました。
Sphinx から完全に reStructuredText を追い出した話
いくつかの記事で紹介されているとおり、 recommonmark を使うと Sphinx で markdown を使うことができます。
- Software Design 2016年4月号 Markdownで始めるSphinx
- MarkdownでSphinxできるようになったので試してみた(前編) - 意識の高いLISPマシン
- Markdownで書けるSphinxの構築 - Qiita
しかし、recommonmark を利用しても、ドキュメント構造(toctree)を記述するためには部分的に reStructredText を使う必要がありました。
.. toctree chapter1 chapter2 chapter3
これは、文法を拡張する仕組みを持たない markdown の (commonmark の)構造上、仕方がないのですが、せっかく markdown でドキュメントを書いているに、一部 reST 記法を使わなくてはならないのは、少しもにょっとしますね。
どうせなら、完全に reST を書かなくてもよい仕組みがほしいところです。
sphinxcontrib-toc
ということで、sphinxcontrib-toc を作りました。
sphinxcontrib-toc は toctree を表現する .toc ファイルを用意することで、reST を書かずにドキュメントを作ることができます。
使い方はたったの 3ステップです。
1. 以下のコマンドでインストールします。
$ pip install sphinxcontrib-toc
2. conf.py で拡張を有効にします。
extensions = ['sphinxcontrib.toc']
3. index.toc というファイルに、ドキュメントを構成するファイルを列挙します。
chapter1 chapter2 chapter3 ...
4. 最後に、不要となった index.rst を削除します。
$ rm index.rst
あとはビルドするだけです。
$ make html
大きいドキュメントを書くときは
大きいドキュメントを書きたい場合は、複数の .toc ファイルを作り、親の .toc ファイルから子の .toc ファイルを参照します。
chapter1/index chapter2/index chapter3/index
そして、子の .toc ファイルにはその章や節を構成するファイルを列挙します。
# 章タイトル section1 section2 section3
子の .toc ファイルでは markdown のタイトル記法を使って章や節のタイトルが指定できます。
(省略した場合はドキュメントのタイトルが適用されます)
sphinxcontrib-toc の仕組み
sphinxcontrib-toc は .toc ファイルを通じて toctree を生成します。
内部ではあたらしいファイルタイプを Sphinx に登録し、.toc ファイルが見つかるたびに sphinxcontrib-toc が呼び出されるようになっています。
そして、sphinxcontrib-toc は .toc ファイルをパースして、toctree を生成します。
これは 1.4 から導入されたソースパーサ(Source Parser)という仕組みを使っています。recommonmark もソースパーサのひとつですから、じつはふたつのモジュールは同種の拡張として実装されています。
.toc ファイルは設定ファイルのような見かけですが、実はドキュメントとして処理されています。
これで完璧!?
さて、これで Sphinx と markdown の組み合わせでドキュメントを作ることができます。
github や qiita、そのたたくさんのプラットフォームで使われている markdown でドキュメントが書けるのです!最高ですね!
と盛り上げてみたものの、実際書いていくと、まだ問題はいくつか残っています。
- recommonmark は CommonMark の範囲のマークアップしかできない
- 表、定義リスト、脚注、顔文字などを書けない
- recommonmark はディレクティブやロールが使えない
前者については、commonmark の仕様上の問題ですので、recommonmark に代わる別の markdown パーサが出てくるのを待つ必要があります。
個人的な実験プロジェクトとして sphinxcontrib-markdown というのを作りかけのままにしていますが、これが完成すると github flavored markdown にも対応したものができるはずです。
興味がある方は手伝っていただけると非常にありがたいです。
後者は markdown の文法をさらに大きく変えることになります。
利便性は高くなりますが、あらたな markdown 方言を生み出すことになります。
実は recommonmark にも AutoStructify という拡張文法が存在します。しかし、こうした方言を利用した場合、別の処理系にはドキュメントを持ち運ぶことができなくなります。
ここはトレードオフになる部分ですので、どのようにマークアップすべきかも含めて、ツール選びを検討すると良いでしょう。
Sphinx コードの半減期と未来予想図
Twitter で流れてきた コードの半減期とテセウスの船 | 開発手法・プロジェクト管理 | POSTD を読んで、興味深かったのでさっそく Sphinx のコードでも実行してみました。
その結果がこちらです。
コードの増え方は時期によって波がありますが、やはり Sphinx もコードの量は右肩上がりに増えています。そして、古いコードは徐々に整理されて少なくなっています。
面白いのは、生き延びた古いコードはある程度安定しているためか、削除されることが少なくなってきている一方で、新しく追加されたコードは入れ替わりが激しい傾向が読み取れます。
Sphinx はまだコードの追加と削除が繰り返される、活発なプロジェクトだということがわかります。
モジュールごとの半減期
さて、せっかくの機会ですから、別の角度からも見てみましょう。
このグラフは Sphinx の主要モジュールの年代ごとのコード比率です。
以下のコマンドでえいやっと計算しました。
$ find sphinx/environment -type f -exec git annotate {} \; | perl -ne 'm/(\d+)-\d+-\d+/; print $1, "\n"' | sort | uniq -c 155 2007 180 2008 128 2009 81 2010 80 2011 6 2012 10 2013 272 2014 42 2015 1159 2016
directive, builders, writers, util, ext, application などの主要なモジュール群は 40〜50% 程度の実装が 2010年までに実装されたものだということがわかります。
一方、domains や environment は 25% 程度とコードが入れ替わっていることがわかります。
また、environment を除くモジュールは約 25% 前後が 2016年にコミットされたものだということがわかります。
ここ 1年であちこちに手を入れていったので、その成果がここに現れているということですね。
さて、2016年に変更されたものでも、ひときわ目立つものがあります。見ておわかりの通り、 environment です。
なんと environment モジュールは 50% 超が 2016年製のコードです。
大きく書き換わった environment モジュール?
いったいこの 2016年、 environment モジュールに何が起きたのでしょう?
種明かしをすると、実はコードの内容は大きく変わっていません。
2016年10月、 https://github.com/sphinx-doc/sphinx/pull/2945/files という PR が取り込まれました。
これは、environment をリファクタリングし、いくつかの細かいモジュールに分割する変更です。
モジュール化にあたり、インターフェースを定義するなどの多少の調整は行っていますが、これまでのコードからは大きく変化はありません。
つまり、この 50% という大きな変更はファイルの分割によって発生しているものでした。
実は git blame が理解していないだけで、そんなに大きい変更ではなかったんですね。
生まれ変わる Sphinx
それでは、どうして Sphinx プロジェクトではこのような変更を行ったのでしょうか?
その答えは Sphinx のコードの複雑さにあります。
Sphinx は数多くの人に使われていて、多くの API によって拡張可能となっているにもかかわらず、その内部は結合度が高く、モジュールの粒度もかなり大きいままです。
現在、このおおきな Sphinx のコアモジュールを整理する動きがあります*1。
特に environment モジュールはコードベースが巨大で、持っている機能や責任範囲も広く複雑です。
担当する責務をいくつか例に上げると、
- ドキュメントソースの読み込み
- ドキュメントの中間データの管理、保存
- その他の中間データ(インデックスやドメインデータなど)の管理、保存
- 目次データの生成
- 更新されたドキュメントソースの検知
- アセットファイル (ダウンロードファイルや画像ファイルなど)の管理
- ログ管理
- パス変換
- ビルド中のコンテキスト情報の保持
- 参照(リンク)の解決
- ドメイン機能の提供
- インデックスの管理、解決
- etc. etc.
などがあります。
あきらかに単一責任の原則に反していますし、メンテナンスをしている側からも非常にわかりづらい、魔窟化したモジュールのひとつでした。
というわけで、あの PR ではこの魔窟を切り広げるための第一歩として投げ込まれたものです。
ただ、これはあくまで一歩目でしかありません。
まだまだ続く Sphinx のリニューアル
今後も Sphinx の構造をシンプルにしていく活動は続きます。
(個人的な) ゴールとして、次のようなことを目標にしています。
Sphinx はさまざまな API を提供する大きなフレームワークの一種です。
Sphinx 自体もそのフレームワークを利用している (HTML 出力や LaTeX 出力などは拡張として実現している) のですが、一方でフレームワーク部分はあまりきれいに整理されておらず、どちらかというとモノリシックな構造でできています。
今後はモジュール化をすすめ、フレームワーク部分をより整理したものを目指していきます。
おそらくその過程で、Sphinx コアはスリムで拡張しやすくなり、整理された API セットがみつかっていくでしょう。
今のところ、このスリムな Sphinx & API セット を 2.0 とすべく、こつこつリファクタリングを繰り返しています。
次は…?
いまはログまわりの改修を進めています。
https://github.com/sphinx-doc/sphinx/pull/3267
Sphinx はメッセージ出力、ログ出力を application, environment という 2大モジュールで経由するようにデザインされています。
そのため、ログを出力するにはどちらかのインスタンスを持っている必要があるため、単にログを出力したいがために application オブジェクトが渡されているなど、無駄に結合度が高い作りになっていました。
この PR では、これらを切り離し、python の logging パッケージで置き換えようとしています。
*1:なんとなく他人事っぽく書いてみたものの、動いているのは自分なので「やってます」が正しいですね
Linux カーネルでの Sphinx 利用法を見てみよう
年末カウントダウン Sphinx 連載、第3弾です。
一切下準備をせずに連載を始めたので、早くも息切れをしています。
だれかと約束をしたわけでもないのに急にアドベントカレンダー的なものを始めるのは、なんだか死に急いでいる気がしてきました。
今回は、今年から Sphinx の利用者に加わったある大型プロジェクトについて紹介しましょう。
そのプロジェクトとは、世界最大級の OSS のひとつである Linux カーネルです。
現在の Linux カーネルドキュメント
今年開催された LinuxCon Japan 2016 で Linux カーネルのドキュメントに Sphinx を使うという発表がありました。
Linus Torvalds氏が登壇、「約10週間のリリースサイクルは続く」 - クラウド Watch
それから約 5ヶ月。現在はどうなっているのか見てみましょう。
Linux カーネルのドキュメントは https://www.kernel.org/doc/ で公開されています。
最初に目に入るのは (new sphinx format) という記述が目に入ります。
そして docbook format の方には (deprecated) の文字が。
というわけで、いまでは Linux カーネルのメインのドキュメントには Sphinx が使われています。
リンクをたどると見慣れた readthedocs テーマで生成されたドキュメントが現れます。
カーネルドキュメントに Sphinx が採用された経緯
LinuxCon 2016 のプレゼンを聞いていた @senopen 氏は当時こんな風にツイートしています。
(当時、仕事中にこれが流れてきてかなりびっくりした覚えがあります)
#linuxcon カーネルのドキュメント。
— せのぺん (@senopen) July 14, 2016
・txtファイル:2000+
・DocBook:カーネルの心臓の記述
・Doxygen形式と似たのKerneldocコメント:55000個。
カーネル開発者がシステムを作った。
make htmldocs
#linuxcon 問題点が。
— せのぺん (@senopen) July 14, 2016
・遅い
・brittle
・設定とmakeが難しい
・他のDocumentation/ディレクトリと統合がない。
最近変えた。
markdonwを導入。その後AsciiDocに切り替えた。
#linuxcon
— せのぺん (@senopen) July 14, 2016
AscIiDoc
利点
・汚いDocBookを回避
・よりよい文書
欠点
・パフォーマンス
・文書館のリンク
・Ruby依存
#linuxcon
— せのぺん (@senopen) July 14, 2016
何がやりたいか。DocBookはやめたい。
簡単なマークアップを使いたい(Markdonw,AsciiDoc,Sphinx)。
フォーマットされていない文書にもそのまま使いたい
東道された文書ツリーを作りたい。
#linuxcon それでSphinxに目をつけた。
— せのぺん (@senopen) July 14, 2016
・コードの文書化に特化
・世界的な利用
・DocBookやLaTeXに頼らない。
で,Sphixを使うことで合意が取れた。
kerneldocコメントはいつも動作して,RST指示文を追加できる。
DocBook から移行をするのは理解できるものの、なぜ asciidoc ではダメだったのか、
Ruby 依存とはどういうことかと疑問に思っていたのですが、
その答えは LWN.net への投稿 で説明されていました。
asciidoc と Sphinx のどちらを採用するかの判断理由として、次のように説明されています。
But it seemed that neither tool would work as-is, or at least we wouldn't be able to get their full potential without extending the tools ourselves. In the kernel tree, there are no tools written in Ruby, but there are plenty of tools written in Python. It was fairly easy to lean towards Sphinx in this regard.
超訳:
Grant Likely summed it up this way: "Honestly, in the end I think we could make either tool do what is needed of it. However, my impression after trying to do a document that needs to have nice publishable output with both tools is that Sphinx is easier to work with, simpler to extend, better supported.”
超訳:
- どちらのツールでも要求は満たせた
- 両方試した結果、Sphinx の方が使いやすく、簡単に拡張できて、サポートがよかった
どうやら、既にカーネルの構成管理ツールとして Python が使われていたので、どちらかといえば Python を使ったほうが楽だろうという判断のようですね。また、Sphinx の方を気に入ってくれたようです。
(サポートが良かった、というのはどういうことなんでしょうかね。特に何かをした覚えはないのですが)
ちなみに Part.2 では移行や、あたらしいドキュメントの書き方について説明しています。
個人的にツボった所だけ抜粋しておきます。
grepping and reading reStructuredText is much easier than the angle-bracketed mess that is DocBook.
reST を grep したり読んだりするのは、DocBook のタグよりはるかにかんたんです。
It's a nice vision, I hear angels singing when I think about it and so on, it's where I want to go.
(数年後、包括的で読みやすいドキュメントができることについて)
そう考えると天使の歌声が聞こえます。
カーネルドキュメントはどのような設定になっているのか
カーネルドキュメントの設定は ここ から参照できます。
読み進めていくと、極めて一般的な、Sphinx の基本設定だけで構築されていることがわかります。
primary_domain が C になっているのは、やはり Linux カーネルですね。
primary_domain = 'C' highlight_language = 'none'
LaTeX のプリアンブル部には、いくつかの設定が指定されています。
- 色やスタイルなどの指定
- landscape 出力の準備
- XeLaTeX 用のフォント設定
また、巨大なドキュメントであるため、PDF もいくつかに分冊して出力するようになっています。
このあたりは Python のドキュメントでも培われた、ドキュメントを小分けにするテクニックをうまく使っているようですね。
latex_documents = [ ('doc-guide/index', 'kernel-doc-guide.tex', 'Linux Kernel Documentation Guide', 'The kernel development community', 'manual'), ('admin-guide/index', 'linux-user.tex', 'Linux Kernel User Documentation', 'The kernel development community', 'manual'), ('core-api/index', 'core-api.tex', 'The kernel core API manual', 'The kernel development community', 'manual'), ('driver-api/index', 'driver-api.tex', 'The kernel driver API manual', 'The kernel development community', 'manual'), ('kernel-documentation', 'kernel-documentation.tex', 'The Linux Kernel Documentation', 'The kernel development community', 'manual'), ('process/index', 'development-process.tex', 'Linux Kernel Development Documentation', 'The kernel development community', 'manual'), ('gpu/index', 'gpu.tex', 'Linux GPU Driver Developer\'s Guide', 'The kernel development community', 'manual'), ('media/index', 'media.tex', 'Linux Media Subsystem Documentation', 'The kernel development community', 'manual'), ('security/index', 'security.tex', 'The kernel security subsystem manual', 'The kernel development community', 'manual'), ]
また、カーネル特有の設定として、いくつかの拡張を利用するようになっています。
extensions = ['kerneldoc', 'rstFlatTable', 'kernel_include', 'cdomain']
ここで指定されている kerneldoc, rstFlatTable, kernel_include, cdomain という4つの拡張はドキュメントと一緒にコミットされている
カーネル専用の Sphinx 拡張です。
それぞれどういう効果のある拡張なのか、ひとつずつ見ていってみます。
カーネルドキュメント専用の拡張
kerneldoc
外部スクリプトである kernel-doc コマンドを呼び出して、カーネルのソースコードからコメントを抽出するディレクティブ kernel-doc を提供します。
抽出したコメントは reST として解釈され、ドキュメントに埋め込まれるようです。
rstFlatTable
list-table のようなリストベースの記法で、なおかつセル結合に対応したテーブル系ディレクティブです。
このページ のサンプルでわかるように :rspan: と :cspan: というふたつのロールを使ってセル結合を行います。
また、セルが足りない場合は自動的に補ってくれる autospan 機能もついています。
やや ad-hoc なマークアップではあるものの、複雑な表を作りたい場合は便利そうです。
kernel_include
環境変数などを含んだパスに対応した include ディレクティブである、kernel-include ディレクティブを提供しています。
おまけ
Linus が reStructuredText を書いているのか気になって調べてみたら、
過去半年で彼が Documentation/ ディレクトリ以下にコミットしたの、これだけだった。
https://github.com/torvalds/linux/commit/852d21ae1fcdf0e4de6b5bfa730d29cb013c7ff3
(その他はすべてマージコミット)
.rst ファイルに symlink を貼ったのも、Sphinx を使ったの一部、ですよね? (弱気)
Sphinx のメンテナになって一年が経過した話
クリスマスが過ぎてから始まる Sphinx アドベントカレンダーへようこそ (嘘)
Sphinx 大型連載第二夜です。
タイトルにある通り、Sphinx のメンテナ活動をして一年が過ぎたので、その話をします。
OSS 開発者のひとつのサンプルケースとして、何かの参考になれば幸いです。
Sphinx のメンテナ活動をはじめました
去年の 12月から Sphinx のメンテナ活動をはじめました。
Python のリリースマネージャ活動が忙しかったからか原作者の Georg の活動が鈍り、
また、その後を継いだ清水川さんも忙しくて身動きが取れなくなっていたことから、
コミット権をもらっていたことだし、パートタイムで手伝うかと思ったことがきっかけでした。
以前からコミット権は持っていたものの、一切メンテナとしての活動をしていなかったので、
徐々にチケットが溜まっていく様子に後ろめたくなったのかもしれません。
増え続けるチケットの戦い
Sphinx は 2015年1月はじめに bitbucket から github にプロジェクトが移動しました。
移動した時点では 1662件だったイシューは、現在 3300件弱あります。
bitbucket 時代は開発開始から 6年間分、そして github 時代は約 2年間分なので、イシューが増加するペースはかなり増していることがわかります。
また、github 移行後の増加グラフを見ると、2015年代は 40件/月程度だったのが 2016年代は 80件/月程度に増加していることもわかります*1。
対する Sphinx のメンテナの人数は、この間あまり変化がありません。
一人ないしは二人。これがアクティブな Sphinx のメンテナの人数です。
2015年末に自分が、そして 2016年に入ってから TeX 使いがひとり、メンテナとして活動をしていますが、メンテナの人数はかぎられているため、絶えず増え続けるイシューに対して常に後手に回っています。
現時点では、増加するペースのほうがやや大きく、チケットが増えていく傾向にあります。
むしろ、難しいイシューはそのまま積まれており、かんたんなイシューから解決していくことが多いので、件数だけでなく濃度も徐々に高まっているという印象があります。
メンテナ活動でやっていること
主にこんなことをやっています。
- バグ修正
- 機能追加
- ドキュメント更新
- PR レビュー
- イシュー対応
- 環境改善 (テスト、CI、その他)
- 定期リリース
- メジャーリリース
- リリース計画立案
- 開発者メーリングリストでのフォローアップ
- ユーザメーリングリストでのフォローアップ (Q&A などなど)
- 関連ライブラリ(docutils)へのパッチ投稿
他のメンテナが機能停止していることもあって、ここ一年は大体自分ひとりで動いていました*2。
我ながらよく頑張った。
その他、Sphinx-Users.jp のコミュニティ活動としてこんなこともやってました。
- Sphinx+翻訳 Hack-a-thon の開催
- Software Design 誌での連載 (執筆は 4回ほど。レビューにはなるべく協力)
- slack で雑談
- 勢いに任せて Sphinx 拡張の開発 (雑誌の題材だったり、slack でのリクエストだったり)
なんというか、本当に Sphinx 漬けな生活を送ってますね。
メンテナという責任との付き合い方
挙げていくとイシューもやることも山積みのように見えますが、オープンソースという場なので、
あまり責任とかは気にせずに付き合っています。
外から見るとずーっと活動し続けているようにも見えるそうですが、
仕事が忙しいときはサボっていたり、旅行に行ったり、飲みに行ったりも適度にしています。
あとひたすらプレミアリーグ見てます。
継続的に活動し続けていくことが大事だという大義名分のもとに、
あくまでマイペースに、ときどき放置しながらも、コードを書いたりイシューに返事をしたりしてます。
ということで、あまりに根を詰めて燃え尽きたりせずに、一年を過ごせたのは良かったと思っています。
引き続き、飽きるまでマイペースに付き合っていくつもりです。
メンテナしていて得すること
僕は個人的にもお仕事でも Sphinx を使っていないので、コードそのものから得られるメリットはありません。
blockdiag を作っていたころもそうですが、どうも自分の使わないコードをいじるのが好きなようです。
強いてあげるとすれば
- 英語の読み書き能力
- Python 能力 (docutils パワー?)
- ハックするネタ
- ドキュメントネタへの興味 (出版とかフォーマットとか)
- メンテナとしての自意識/自尊心の拡大
あたりが得したことでしょうか。
特に、英語については反射能力がついたと強く感じています。
飛んできたイシューを片っ端から読んで、コメント書いてを繰り返しているので
英語に対するハードルがかなり下がって、フットワークが軽くなっています。
語彙や単語などはあまり変わっていないのですが、これもひとつのレベルアップと言ってもよいはずです。
あとは、ハックネタが提供されたことで常に Sphinx のことばかり考えるようになってますね。
何者かになりきれない自分にはいい素材が提供されたと思っています。
メンテナをして損したこと
とにかく時間が吸い取られます。
blockdiag や testing.mysqld など、他にもメンテナンスすべきパッケージがあるはずなのですが、
そっちに時間を割くことができていません。
Sphinx 以外の他のこともやってみたい、というぼんやりとした考えもあるのですが、
イシューの物量に押し流されているところもあります。
メンテナに必要な知識
先程挙げたように、やるべきことは山積みで、そして広範囲です。
コードをいじるにしても、docutils や Python そのものの知識もそうですが
出力フォーマット(HTML や LaTeX、EPUB)や Python のコード解析、フレームワーク化など、
Sphinx が扱っている分野そのものがとても広いので「この知識を持っていればよい」というものではありません。
むしろそんなスーパーマンはいないので、みんなの力を結集して解決していきたいところです。
ですので、なにかひとつでも継続的に関わることができるのであれば、
胸を張って手伝っていけばよいと思います。
最近、usaturn 氏がイシューのトリアージに参加するようになりました。
Sphinx のトリアージを頑張ってやっていこうと思います https://t.co/HZV35ILJFl #sphinxjp
— うさたーん (@usaturn) 2016年12月10日
イシューに溢れかえった Sphinx では、再現確認や一時切り分け、単なる質問の対処などであっても
やるべき作業が減るのでとても助かります。
メンテナであれば〜〜ができなくてはならない、なんて自分でハードルを上げずに
やれることを手伝っていくスタンスから始めるのが良いと思います。
ひとつ付け加えるとするなら、英語の能力があると心理的なハードルが下がります。
でも、先日 Google 翻訳がパワーアップをして、かなりハードルを下げてくれたので
そんなに気にしなくても良いかもしれません。
OSS メンテナのモチベーション/キャリアプラン
Sphinx のメンテナとして活動していることは誇らしくもあるのですが、
一方で自分のスキルアップ、キャリアアップには役立っていないと感じている面もあります。
Web フレームワークやインフラなど仕事のメインとなるツールではないこと、
自分の業務には(いまは)ほとんど関係のないツールであること、
ドキュメンテーションというニッチな(あまり人気のない分野の)ツールであることなど、
メンテナ活動を続けることが自分のキャリアにあまり貢献をしていないと感じることがあり、
たまに隣の芝生が青く見えることがあります。
この記事は、そうした自分を写した鏡を見たような気分になりました。
A Million Hello Worlds - steps to phantasien
自覚しているだけに、胸に刺さる / “自分を突き動かす社会的欲求の必死さがない。それは弱さでもある。” / A Million Hello Worlds - steps to phantasien https://t.co/pYTjz7Hxsz
— Takeshi KOMIYA (@tk0miya) 2016年12月10日
また、ドキュメンテーションツールとしても、markdown 全盛のこの世の中、
Sphinx に将来があるのかどうか、自分自身あまり信じられていないこともあって
もやーっとしている思いがないわけでもありません
*3。
僕の今の居場所は Sphinx なのは間違いないけど、同時に僕は Sphinx には先のない、斜陽感を感じていて、このままメンテナを続けていくと一緒に沈んでいってしまうのではないかという不安感がある。
— Takeshi KOMIYA (@tk0miya) 2016年12月10日
かと言って見捨てるわけにもいかないし、他に興味がある技術があるわけでもなし、ずるずるとメンテナンスを続けているという部分は少なからずある。
— Takeshi KOMIYA (@tk0miya) 2016年12月10日
十分優れたソフトウェアだし、利用者もかなりいるし、手を入れなくちゃならない/入れたい課題も山盛りだし、やりがいは十分にあるはずなのだけど、自分では使ってないし、時間は食うしでなんでお前メンテしてるの? 動機は? と尋ねられてもなんとも答えられない。
— Takeshi KOMIYA (@tk0miya) 2016年12月10日
言葉にするとあまりに正確じゃなくてこまる部分ではある。嫌なわけでも、義務でやってるわけでもないし、楽しんでやってる部分もある。そりゃ苛立ちとかそういうのもたまにはあるけど。でも、僕は Sphinx の先に道があるとは信じられていないので、閉塞感みたいなのがあるのよね。
— Takeshi KOMIYA (@tk0miya) 2016年12月10日
お金持ちになりたいわけでも、人気ものになりたいわけでもないけど、どうせなら多くの人を支えるツールに関わりたいよなーという、承認欲求なのか社会的欲求なのかそういうのにさらされる瞬間がある。が、既に十分に使われてはいるし、流行っている方に興味があるわけでもないしねえ。
— Takeshi KOMIYA (@tk0miya) 2016年12月10日
隣の芝生は青白く発光している
— Takeshi KOMIYA (@tk0miya) 2016年12月10日
では、モチベーションってなんだろう、と考えたのがこちら:
@voluntas 自分が使うツールって、ほとんど作ってないなあ。きっと根っからの SIer なんでしょw
— Takeshi KOMIYA (@tk0miya) 2016年12月10日
パソコン通信の時代から、誰かに使ってもらえるツールを作ることに憧れていたので、
それを考えるといまはそれを満たしているので満足してるんじゃないか、と自己分析しています。
結論:僕の OSS 活動はキャリアと関係なかった。
キャリアの問題とか関係ないよーな気がしてきた。みんなは修練に励んでステップアップしてるけど、僕はそっちに興味が持てなくて盆栽いじりしてるだけだもんなあ。閉塞とかかっこいい言葉を使ってないで、もっとシンプルに、なにもしなくても給料上がらないかなあって言えばいいんだ
— Takeshi KOMIYA (@tk0miya) 2016年12月10日
承認欲求が満たされないと、雪が降る夜にマッチを擦って承認を満たして、翌日凍死体として発見されることになる。早急にわかり合うべきだ。
— Takeshi KOMIYA (@tk0miya) 2016年12月19日
Sphinx に mypy の type annotation を導入した話
今年はアドベントカレンダーにも参加していないし、こたつに入ってだらだら過ごそうかと思っていたら、なんか書けと煽られました。年末ですね。
@tk0miya qiita の記事まだー?
— Tetsuya Morimoto (@t2y) 2016年12月25日
何を書こうか思考をめぐらした結果、mypy を Sphinx に導入した話でも書くことにします。
mypy については @t2y の 紹介記事 、翻訳記事が非常に参考になりました。
ですので、この記事は @t2y へのアンサーソングです。
なお、「導入した話」と名付けてはみたものの、まだ 100% 対応したというわけではないので、試行錯誤の様子と愚痴を書き留めていきます。
Sphinx に type annotation をつけてみた
Sphinx では現在、 master ブランチに対して type annotation がつけられています。
一方、stable ブランチ、つまり現行リリース版である 1.5 系には追加されていません。
これが type annotation を追加する PR です。
Sphinx はそれなりの規模のコードベースがあるため、差分もかなり大きいものになっています。
mypy について調べつつ、実験しつつ、仕事をこなしつつ、一週間ぐらいで片付けた記憶があります。
本来であれば絶賛メンテナンス中の stable ブランチに入れた方が運用しやすそうだったのですが、
1.5 のリリースに向けた feature freeze 中で、こんなにでかい差分を取り込む勇気がなかったので
日和って master ブランチに突っ込むことにしました。
その結果、
- stable ブランチでバグ修正した
- master ブランチに取り込む
- アノテーションを(必要に応じて)調整する
というステップを踏むことになりました。
できれば安定版にアノテーションを書くと良いでしょう。
導入するときに試行錯誤したあれこれ
アノテーションの書き方
Sphinx は Python 2.7 と 3.4 以降をサポートしています。
そのため、導入するときはどういう書き方がよいのか、あれこれ試しました。
本来は Python 3 で導入された関数アノテーションを使って型を宣言していくとよいのでしょうが、
Python 2.7 では文法エラーになってしまいます。
ですので、Python2 でも利用できるようコメントベースのアノテーションを利用することにしました。
def docname_join(basedocname, docname): # type: (unicode, unicode) -> unicode return posixpath.normpath( posixpath.join('/' + basedocname, '..', docname))[1:]
コメントベースのアノテーションは関数の定義の直後(docstring の前)、変数定義の直後に # type: 宣言を書きます。
詳しくは mypy のドキュメントを読んでください。
mypy の設定
Sphinx での設定は mypy.ini にまとめました。
どこかの記事で見かけた設定をそのまま持ってきたものです。
一点、意識的に設定したのは python_version = 2.7 です。
当初、python 3.5 モードで動かそうと思っていたのですが、妙な型エラーがでてしまったのでとても追いかける気力になれなかったことに起因しています。
import codecs filename = u'example.dat' with codecs.open(filename, 'r', encoding='utf-8') as fd: pass
$ mypy --python-version 2.7 example.py example.py:4: error: Argument 1 to "open" has incompatible type "unicode"; expected "str" $ mypy --python-version 3.5 example.py example.py:4: error: "StreamReaderWriter" has no attribute "__enter__" example.py:4: error: "StreamReaderWriter" has no attribute "__exit__"
from docutils import nodes root = nodes.Node() root += nodes.Text()
$ mypy -s --python-version 2.7 example.py # 何も言われない $ mypy -s --python-version 3.5 example.py example.py:3: error: "module" has no attribute "Node" example.py:4: error: "module" has no attribute "Text"
前者は標準ライブラリの型を定義している typeshed の定義不足っぽいのですが、後者はさっぱり謎です。
深追いはしなかったのですが、いきなり心が折れそうになりました。
str と unicode
みんなだいすき str と unicode の違いとも格闘しました。
$ cat example.py from six import text_type var1 = 'abc' # type: str var2 = text_type(var1) # type: str var3 = u'def' # type: str $ mypy example.py example.py:4: error: Incompatible types in assignment (expression has type "unicode", variable has type "str") example.py:5: error: Incompatible types in assignment (expression has type "unicode", variable has type "str")
unicode リテラルに str 型だというヒントを与えると蹴り飛ばされます。
(オプションは省略されていますが、ここからは前述の mypy.ini を使っています。記憶が確かなら)
他にも str から作成された正規表現と unicode から作成された正規表現で、それぞれ引数の型が微妙に違うというボディブローをくらうことがあります。
import re str_pat = re.compile('abc') str_pat.match('abc') str_pat.match(u'abc') unicode_pat = re.compile(u'abc') unicode_pat.match('abc') unicode_pat.match(u'abc')
$ mypy example.py example.py:5: error: Argument 1 to "match" of "Pattern" has incompatible type "unicode"; expected "str"
救いはありません。
正規表現にかぎらず、あちこちの関数で unicode を受け入れられないという警告が出ます。
なお、型の世界では警告が出ますが、スクリプトを実行するともちろんちゃんと動作します。
type annotation というきっちりとした世界にも、本音と建前という人間くささが潜んでいると思うと、こころが暖かくなりますね。
Python 3.5 モードで動かすと str に統一されるので、この苦しみからは解放されるはずなのですが、先ほど書いたとおり 3.5 は 3.5 で茨の道感があります。
救いの神 type: ignore
既存の巨大なアプリケーションに対してアノテーションを書いていくのは、非常に根気が必要です。
mypy に不慣れだというのを棚に上げて前に進まないと、徒労感が積み重なっていきます。
そんなときに、人類の救世主として登場するのが # type: ignore アノテーションです。
このアノテーションの手にかかれば、あんなに解決に苦労した警告も、たちどころに解決します。
$ cat example.py import re str_pat = re.compile('abc') str_pat.match('abc') str_pat.match(u'abc') # type: ignore unicode_pat = re.compile(u'abc') unicode_pat.match('abc') unicode_pat.match(u'abc') $ mypy example.py # 何も言われない
やりましたね。ひとまずカバー率を上げるために、心を鬼にして先に進みましょう。
ちなみに Sphinx はまだカバー率が 100% に達していないので、そのまま塩漬けになっています。
List と Tuple
話はちょっと代わりますが、「ユーザ名、メールアドレス、得点」というデータがあるとき、あなたならどういうデータ構造を作りますか?
リストを使いますか? タプル? それともクラスを定義する?
また、イベントを進めると得点が加算されるような場合はどうでしょう。
Sphinx では、いくつかの箇所で簡単なデータのペア/トリオを表現するのにリスト(配列)やタプルを使っていました。また、途中でデータの内容が変わるようなところにはリストが使われていました。
さて、こうったデータ構造を使っている場合、type annotation はおもむろに右ストレートを放ってきます。
mypy の type annotation では
- リストは無限個のデータ列。データ型をひとつだけ指定する。
- タプルは有限個のデータ列。データ型はそれぞれ指定する。
という前提があるため、さきほど例に上げた「ユーザ名、メールアドレス、得点」というデータはリストでは表現しづらいもののひとつです。
こうした場合の回避策のひとつに Union があります。
Union は複合型を表します。
# リストの場合 user1 = [username, email, score] # type: List[Union[unicode, int]] # タプルの場合 user2 = (username, email, score) # type: Tuple[unicode, unicode, int]
Union を使うと「文字列、文字列、数値のリスト」ではなく「文字列か数値のいずれかのリスト」として表現できます。
Union 最高!と言いたいところですが、これを書いた瞬間に型情報が "あいまい" になる欠点があります。
user1[0] も user1[1] も user1[2] も「文字列か数値のいずれか」のデータになります。
そのため、たとえば unicode を受け取るような関数に指定すると型エラーが発生します。
$ cat example.py from typing import Union def hello(name): # type: (unicode) -> None print("Hello %s." % name) user = ["tk0miya", "tk0miya@example.com", 100] # type: List[Union[unicode, int]] hello(user[0]) $ mypy example.py example.py:9: error: Argument 1 to "hello" has incompatible type "Union[unicode, int]"; expected "unicode"
さて、果たして Union は最高でしょうか?
個人的な結論としては、Union は容量用法をよく守ってインターフェースに対してのみ使うべきで、
データに対しては使うべきではないです。
List と Tuple と Dict と Set と Sequence と Iterable と Iterator と Generator 。
データ集合を扱う、いわゆるコンテナ的なデータをあらわす型はいくつもあります。
List、Tuple、Dict、Set というデータ型そのものと、Sequence と Iterable という振る舞いを表す抽象型、そして Iterator と Generator という動的なデータ集合の操作インターフェースが該当します。
データに対してアノテーションする際はこの違いを意識する必要はないのですが、関数に対してアノテーションを書く場合はこの微妙な違いを意識する必要があります。
例えば、連続したデータを受け取る場合は Iteratable を指定します。
Iterable は List, Tuple, Dict, Set, Iterator, Generator を受け取ることができます。
一方、インデックス指定でのアクセス可能な場合は Sequence を使います。
Sequence には List, Tuple, Dict が該当します。
関数インターフェースには抽象型を指定しておくと、その関数を使う側のコードの自由度が上がるので、
引数のアノテーションにはなるべく抽象型を使って指定すると良いでしょう。
(返り値は型が定まるはずなので、具体的な型を指定する方がよさそうです)
型アノテーションで実装の歪みに気づく
ちなみに、Sphinx にはビルド対象のファイル名の集合として docnames というデータがあります。
この docnames 、あるところでは List として、あるところでは Set として、またあるところでは Tuple としてデータを定義したり、操作したりしています。
そのため、これらの整合性を取るのが非常に難しく、具体的なデータ型をアノテーションしても怒られ、また抽象的な型を指定しても怒られるという、ひどい状況になっています。
これはアノテーションだけでは解決できず、コードを直す必要がある貯め、現時点でも型エラーが出続けたままになっています。
このように、型アノテーションをつけていくと、実装の歪みに気づくことができます。
辛い現実ですが、涙を拭いて立ち上がりましょう。
助けてください。
ダウンキャスト
いまだに書き方がわかっていないもののひとつにダウンキャストがあります。
$ cat example.py class Base(object): pass class Foo(Base): pass class Bar(Base): def say(self): print("Hello!") items = {} # type: Dict[unicode, Base] items['foo'] = Foo() items['bar'] = Bar() items['bar'].say() $ mypy example.py example.py:14: error: "Base" has no attribute "say"
Sphinx では、あちこちにこうした「共通の親クラスを返す」インターフェースが定義されています。
すべてのデータに対して共通の処理を書く場合は、これで特に構わないのですが、
上記のように特定のインスタンスの処理を呼び出したい場合は、これをきれいに書く術が見つかっていません。
ドキュメントによると Cast が使えそうな気がしています。いずれ時間を取って試してみる予定です。
type annotation と flake8 の衝突
mypy はアノテーションコメントの途中での改行を認めていないため、一行に書かねばなりません。
そのため、複雑な構造をしたデータや、多くの引数を取る関数の場合、アノテーションコメントが長くなります。
その結果、flake8 と衝突します。
結論としては、NOQA コメントを使ってくさいものに蓋をします。
self.versionchanges = {} # type: Dict[unicode, List[Tuple[unicode, unicode, int, unicode, unicode, unicode]]] # NOQA
定義があまりに長い場合は、それそのものがコードのあやしいにおいのひとつかもしれません。
クラスや namedtuple を定義すると、型的にも実装的にもシンプルになる可能性があるので、
記述が長い場合は実装を見直すと良いかもしれません。
また、alias を使って型に名前をつけるのも良さそうです。
循環 import 問題
ユーザ定義の型をアノテーションするには、そのクラスが import されている必要があります。
import しないと undefined だと、肘打ちされます。
$ cat example.py obj = None # type: Sphinx $ mypy example.py example.py:1: error: Name 'Sphinx' is not defined
しかし、型アノテーションするために import を繰り返していくと、そのうち循環 import が発生してしまいます。
アノテーションを書くためにプログラムが壊れてしまうという本末転倒なできごとです。
これを回避するために Sphinx では、次のように回避をしています。
mypy は静的なコード解析をするので、実際に import をしなくともアノテーションできるという特性を生かして、 "import したふり" をします。
if False: # For type annotation from typing import Any, Callable, IO, Iterable, Iterator, Tuple, Type, Union # NOQA from docutils.parsers import Parser # NOQA from docutils.transform import Transform # NOQA from sphinx.builders import Builder # NOQA from sphinx.domains import Domain # NOQA
また、flake8 先生に踏みつけられないように NOQA をするという涙ぐましい努力も同時に行います。
ad-hoc 過ぎますが、いずれ mypy が改善されることを期待して、積極的に目をそらして行きていきましょう。
mypy を実行する際はコード全体に対して実行する
アノテーションを書き始めたとき、最初はサブディレクトリに対して mypy を実行していました。
$ mypy sphinx/util ...
そして、あるサブディレクトリの型エラーがなくなったら、次のディレクトリに進むということを繰り返しました。
そうして、ファイル単位、ディレクトリ単位でアノテーションをつけていったのですが、
ひととおりアノテーションをつけ終わったあと、コード全体に対して mypy を実行すると型エラーが検出されました。
$ mypy sphinx
# => sphinx/util で型エラー!
これは、mypy の -s オプション (--silent-imports) を使って import しているコードの型チェックをスキップしていることによるものです。
コード全体に対して mypy を実行すると、これまで参照していなかったコードも使って型チェックをおこなうようになるため、一度アノテーションをつけた部分についても型エラーが検出されることがあります。
mypy はやや実行に時間がかかるため、アノテーションのつけはじめの段階では、
ディレクトリ単位などで書いていき、型エラーを減らしていくアプローチがよいと思いますが、
ある段階で全体に対して実行すると良いでしょう。
ある程度件数を減らしていったはずなのに、あるモジュールのアノテーションを書くと、また揺り戻しのように件数が増えたりして、心が折れそうになりますが涙をこらえてがんばりましょう。
まとめ
Sphinx にアノテーションを導入しようとして試行錯誤して苦しんだ記録を残しました。
まだ型エラーは残っていますし、すべてのデータ、インターフェースにアノテーションしたわけではありませんが、今のところこんな感じになっています。
$ grep -r type: sphinx | wc -l 3166 $ grep -r 'type: ignore' sphinx | wc -l 397
この記事では、試行錯誤した内容をひたすら書き残したので、つらいだけで、あまり便利そうには見えないのですが、
アノテーションを加えることで、実装の見直しになったり、バグに気づくきっかけとなりました。
また、こうして意識的に型を書いてみることで、確かに型付けは便利であることの気づきにもなりました。
Sphinx は既に巨大なコードベースがあるため、他の言語に引っ越すことは容易ではないので、
こうしてアノテーションでヒントを与えるアプローチは非常に役に立つと感じています。
まだ型エラーが存在するため CI に組み込むところまではたどり着いていないのですが、
引き続き改善を続けていけると良いと考えています。
この記事がこれから mypy を試す人の助けになることを祈って。
参考:
ちょうど、先日も mypy の導入記事を見かけました。見てない方は、一緒に見るといいと思います。
mypyやっていくぞ - Qiita
2014-15年を振り返る / 2016年の抱負
あけましておめでとうございます。2年ぶりの抱負エントリです。
去年は正月の時期を逃してしまい、仕方がないから旧正月に書くことにするか!と思っていたら仕事が忙しくて完全にタイミングを逸してしまいました。
2014年の抱負 として挙げたのは
- blockdiag のバグ/タスクを減らす
- なにか使えるツールをリリースする (少なくとも一本以上)
- 技術書を読み続ける
- 新しい言語でなにかツールを書く
- コードを書く時間を維持する
- 環境整備をしよう
- あたらしいツール/フレームワーク/サービスに触る
- 体重を落とす (目標 -3kg)
の8個でした。
この 2年間は遊んで過ごしていたこともあって、あまり成果らしい成果はでていないので
振り返るほどでもないのですが、反省を兼ねてざっと振り返ります。
blockdiag のバグ/タスクを減らす:✕
完全に放置状態ですね。2年間まるまる手を付けていません。
なにか使えるツールをリリースする (少なくとも一本以上):△
調べてみると
- 2014
- 2015
- flake8-config
- sphinxcontrib-imagehelper
- testing.elasticsearch
- sphinxcontrib-apiblueprint
- sphinxcontrib-markdown
とパッケージを作っていたみたい。
どれもちょっとしたツールで、使えるツールかと言われると悩ましいですね。
そういえば、Sphinx の numfig を作った流れでコミッターになりました。
あまり活動していなかったのですが、年末からぱたぱたと活動を再開しています。
あと、ずっと git 嫌いだったのですが bitbucket の凋落っぷりをみて、あきらめて git + github にスイッチしました。
技術書を読み続ける:◯
安定して新宿 Book-a-thon を開催しています。
読んだのは以下の本らしい。
- 2014
- 2015
- 日本語組版処理の要件 (2014年からの続き)
- DDD Quickly
- リーダブルコード
- Amazon Web Services クラウドデザインパターン設計ガイド
- Amazon Web Services クラウドデザインパターン実装ガイド
- ハイパフォーマンスブラウザネットワーキング
- 実践ドメイン駆動設計 (読み途中)
読んだ中では日本語組版処理の要件が本当におもしろかった。
普段慣れ親しんでいる書籍について、考え方やルールを理解できてとてもいい資料です。
(厳密には本ではないけど、まあいいじゃない)
あと、読む方ではなく、書く方もちょっとやりました。
Software Design の Sphinx の連載記事に 2本ほど寄稿しました。
コードを書く時間を維持する:✕
サボってました。
環境整備をしよう:△ or ✕
ローカルにあれこれ入れたくないとずっと嫌がっていた homebrew を入れることにしたのは
ビッグチェンジだったかもしれません。
体重を落とす (目標 -3kg):◯
がくっと落ちました。-5kg ぐらい。
完全に ingress のおかげです。
最近は忙しくて ingress をやる時間がとれないのですが、その後安定しているのでリバウンドはなさそうな感じ。