2016/05/14

TeXと10年戦ってわかったこと

世間では「TeX」と一口に言われているけど、実際には3つの異なる側面があります。10年以上TeXで何かやってきたわけだけど、「TeXはアレ」の本質はこの3つがごちゃごちゃになってるとこかなと思ったりしているので、書いてみました。

  • マークアップ形式としてのTeX
  • 組版エンジンとしてのTeX
  • プログラミング言語としてのTeX

TeXの話をするときって、これら3つが案外と区別されていないなあと感じることがあります。具体的には、組版エンジンとしての機能について言及している場面でマークアップ形式の話を持ち出されたり、マークアップの話をしているときに名前空間がないからダメといった感じでプログラミング言語として批判されたり、ようするに、あまり建設的ではない。

もちろん、TeXというエコシステムについて何か言うときにはこの3つが不可分で、それぞれを切り出して論じてもしょうがないんだけど、まあポエムなので気にしないことにします。

マークアップ形式としてのTeX

TeXのマークアップというと\{}がやたらに出てくるアレを思い浮かべると思いますが、「いわゆるTeXのマークアップ」と見なされているものに厳格な文法とかルールはありません。 よく、「XML自体はマークアップではなくマークアップを作る仕組みを提供するものだ」と言いますが、TeXはさらにひどくて、シンタックスさえもが自分の知っているTeXでない可能性があるのです。

現状のTeXは、いろいろな人や団体が過去に考案してきた「マークアップ」を、使う人が自分のユーザ文書の用途に応じて混ぜて使っている状態だといえます。 なので、たとえば「TeXのマークアップのパーザがほしい」といった要件を満たすものがあるとしたら、それはTeXそのものになります。 この、ユーザが目的に応じていくらでも変更できる余地がある、というのが、マークアップとしてのTeXの革新的かつ悲劇的な点なのかなと思っています。

とはいえ、だいたい用途ごとに「デファクト」のマークアップはあって、たとえば文章そのものに対する基本的なマークアップの文法でいま主に実用されているのはLamportさんが拡張したLaTeXです。 この、LaTeXで導入され、現在に至るまで主に使われている「いわゆるTeXのマークアップ」は、なかなかよく考えられているなあと思います(もとをただせばScribeのアイデアだし)。 数式に対するマークアップも、Knuthが考えたオリジナルTeXにおける数式の書き方を延々と使っているわけではなく、現在ではアメリカ数学会AMSが拡張したやつが広く使われています。 TeXの中で図を使うためのマークアップは、文章や数式とは別体系で、これはいまはTikZが主流になっています。

拡張したマークアップをパーズするための仕掛けはプログラミング言語としてのTeXで作ります。 また、その出力であるレイアウトは組版エンジンとしてのTeXで実現します。

というわけで、TeXのマークアップはひどい、ふつうの人には無理、という意見は、種々のマークアップが混在していることからくる感想なのかもしれないと思います。 \{}$はオリジナルのTeXに沿って使われることが大半なので、どれも構文は緩く似た感じになり、「えー、なんでこんな一貫性がないのー」って感じますよね。 できることに制約がないので、たとえば「○○用のマークアップとかないし、別の表現にするか」という発想にならず、「○○したいだけなのにこんな複雑なことをしなければならないのかくそが」という感想になりやすいという面もあると思います。

文章に対するマークアップとして見た場合、LaTeXのマークアップは別に筋は悪くはないんじゃないですかねえ。 現代的な構造化もできるし、もちろん必要に応じて拡張もできる(←そこがアレ)。 「マークアップ形式拡張のためのAPIを設計するセンス」を養う本とかどこかにないかなあ。

組版エンジンとしてのTeX

一部のアレな人以外、TeXを使う目的は組版でしょう。これについては、あまり批判的な人はいない気がします。 TeXは当初から組版のための要件がかなり高く、しかもそれが一通り実現されています。 さらに、それなりに長い歴史のなかで、本職の組版技術を知る人たちが自分たちの必要とする付加的な要件を本気で実現してきました。 そのため、ほとんどの人にとっては、世界中の多くの言語で実用的なブラックボックスとして機能できていると思います (ふだん使っているTeXが組版についてブラックボックスに見えない人は、マークアップとしての側面と混同しているか、ただの組版のプロでしょう)。

プログラミング言語としてのTeX

なんというか、KnuthはもともとTeXをプログラミング言語として設計するつもりはさらさらなかったんじゃないかなあと思います。 自分の秘書でも使えるようなマークアップを自分で後から定義できるだけの機能を詰め込んだら、必然的にチューリング完全になってしまったというか。 あるいは、組版エンジンとして必要だろうなーと思う機能を作りこんでいて、その機能を外部からつつけるようにしたら、必然的にチューリング完全になってしまったというか。 とにかく、プログラミング言語としてのTeXにまともな設計思想はない気がするし、プログラミング言語として使いやすくする開発とかもされてこなかった。 なので使いづらいのは当然だし、使いづらさを楽しむのもまた一興かもしれません。 そもそもこれだけの機能がなかったら、後に続く人たちが「自分たちも仕事で使える」ものに魔改造できなかっただろうし(←その結果がアレ)。

とはいえ、そうも言ってられないという人たちは一定数いて、その先鋒がLaTeX3プロジェクトです。 それこそもう10年以上の歴史があるけど、過去のTeX資産を壊さないように慎重に開発が進められているので、いつになってもLaTeX3は完成しません(←だからアレ)。

とはいえ、LaTeX3のうちプログラミング言語として利用できる部分については現在のLaTeX2eでも使える状態にあります(expl3という)。 使いやすいかどうかはともかく、名前空間が実現できたり、関数っぽいものが定義できたり、プログラミング言語としてはふつうになってると思います。 次の10年に期待ですね(それまで選択肢がTeXしかないのも微妙だけど)。

2016/05/02

執筆・編集のためのGit(GitHub)ワークフローを考えてみた

まとまった量の文章を執筆・編集するのにバージョン管理システムを使うことは、少なくとも技術文書においては特別なことではなくなりました。 原稿が汎用のテキストファイルの場合には、バージョン管理システムとして、GitやMercurialなどのソフトウェア開発用のツールを使いたいことが多いと思います。 実際、GitHubやGitBucketを利用して技術書やドキュメントの原稿を共同執筆するという話はとてもよく聞きます(知っている世間が狭いだけかもしれないけど)。

とはいえ文章の執筆・編集という作業には、プログラムのソースコードを開発する作業とは違う側面もいっぱいあります。 そのため、ツールとしてはソフトウェア開発用のバージョン管理システムを利用する場合であっても、そのワークフローについては、執筆・編集ならではの工夫が多少は必要なのかなと考えています。

もちろん、同じソフトウェア開発でもプロジェクトの種類や参加者の考え方によって最適なワークフローは異なるだろうし、 ソフトウェア開発で成功するワークフローが執筆・編集には適用できないなんていうことでもありませんが、 こうやったら技術書を作る時にうまくいった、あるいはうまくいかなかったという経験に基づく考察をここらでいったん公開しておくのは悪くないかなと思ったので、まとめてみました。

なお、あくまでも経験に基づいた独断を書きなぐったものなので、これが正解と主張するものではないし参考文献とかもありません。 また、バージョン管理システムとしてGit(GitHub)が前提の書きっぷりになると思いますが、これも自分の経験に基づく話だからというだけです。 が、だいたいどのバージョン管理システムでも通用する話であろうな、とは想像しています。

目次

  1. 正史モデル:伝統的な文章のバージョン管理ワークフロー
  2. 執筆・編集というプロジェクトの実態
  3. 「じっくり読まなければ何となくいい感じ」が分水嶺
  4. 執筆・編集にとって最適(いまのところ)だと思っているGitワークフロー
  5. 余談:「じっくり読まなければ何となくいい感じ」に到達したことを知るには

正史モデル:伝統的な文章のバージョン管理ワークフロー

旧来、印刷して出版される文書は、初校、再校、念校、下版といった形式でバージョン管理されていました。 複数の関係者がいる場合には著者の代表や編集者が全員の意見(赤字)をマージして一つの修正指示を練り上げ、それが次の校正に反映されるというワークフローです。 つまり、正史が一つだけ存在するモデルです。

この伝統的な正史モデルのワークフローに、バージョン管理システムを適用したいとして、いちばん分かりやすいのは次のようなマイルストーンの細分化でしょう。

従来であれば数日から数カ月おきに確定するバージョン(初校とか再校とか)の間に、バージョン管理システムを使って細かくコミットを重ねていくイメージです。 さらにこのワークフローを突き詰めれば、初校や再校といった従来のマイルストーンの役割も消えて、全期間にわたってコミット履歴だけが並ぶことになるでしょう。 もちろん何らかのマイルストーンは設定することになるでしょうが、いずれにせよワークフローとしてはあくまでも一直線です。

このワークフローでは、みんなが同一のmasterに対してコミットを重ねていくため、誰かの作業内容が妥当であるかどうかを意識的に確認する役割の人がいるのがベターです。 ただし経験では、そのような役割を特定に人に押し付けるのではなく、関係者全員が「あれっ」と思った時点で履歴を検索して適宜blameや再修正をかけられるような空気があるほうが、うまく回ることが多かったと思います。 そのような空気は、関係者どうしの広帯域なコミュニケーションを通じて醸成されますが、 メール(メーリングリスト)、バグトラッキングシステム(TracやGitHub issue)、会議システム(SlackやSkype)を複数併用していた場合にわりとうまくいっていた印象があります。

執筆・編集というプロジェクトの実態

正史モデルのワークフローには、従来型の延長なので比較的導入しやすいというメリットがありますが、制限もあります。 このワークフローでは、どのコミットも、それまでの作業を塗りつぶして上書きする作業として扱います。 あとからコミット単位でrevertしたりrebaseしたりすることをあまり考慮していないワークフローだということです。

せっかくバージョン管理システムを使うのだから、作業の内容を明確化してブランチを切ったり、 それをあとからチェリーピックしたりマージしたり、逆に過去に加えた変更をなかったことにしたり、 そういうことができるほうがうれしいですよね。 正史モデルに拘泥してアドホックな上書き修正を積み重ねるのではなく、修正の意図や作業内容に応じてコミットをきっちり切り分けることで、原稿の変更による手戻を防いだり差分を見やすくしたりできるはずです。

ところが、経験上、文章の執筆・編集では作業内容ごとにコミットを整理したりブランチを切ったりするのが難しいなあと感じる場面がとてもよくあります。 文章の修正を局所的で互いに独立したコミットに収めることができず、コミットどうしが密に結合したり、一見すると巨大すぎるような修正コミットが発生したり、他の人の作業と競合したりしがちなのです。

もちろん、だからといって文章の執筆・編集では正史モデルで徹頭徹尾やるべきという話ではありません。 これまでの経験で感じるのは、うまくコミットとして切り分けられる問題もあれば、コミットとして切り分けるのが徒労でしかないような問題もある、という単純な事実です。 この二種類の問題を整理したところ、執筆・編集という作業には2つのフェーズがあり、この両フェーズを意識するとそこそこ最適なワークフローが説明できそうだなと気がつきました。

執筆・編集には2つのフェーズがある

正史モデルの説明では、「脱稿」以降のワークフローについてバージョン管理システムを使うかのように説明しましたが、あれは嘘です。 バージョン管理システムを使って、共著者、編集者、外部のレビュアーによる共同作業を始めるのは、脱稿よりも前の段階、具体的には「内容をすべて書き出した」時点であるべきです。

ここが重要なんですが、「内容をすべて書き出した」時点は脱稿とは異なります。 「内容をすべて書き出した」時点は、ソフトウェア開発でいうと、まだ最低限の機能がコンパイルすら通っていない状態です。 ここから待っているのは下記のような作業です。

  • 段落や節の見直し、統廃合、順序の入れ替え
  • それにともなう文章の書き直し、表現や語調の調性
  • 全体にわたる用字用語(漢字の使い方とか)の統一、誤用や誤植の修正
  • タグやメタ情報(ルビとか索引とか)の修正、追加

各作業について具体的な内容には踏み込みませんが、ようするに、全体にわたる読み直しとリライトを何度か繰り返すことで、これらの問題をひとつずつ潰していく必要があります。 そうやって確認と修正を繰り返していると、あるときふいに原稿が「じっくり読まなければ何となくいい感じ」になっていることに気づきます。 このへんでようやく、ソフトウェアでいえば警告を無視すればコンパイルが通った状態、つまり本当の意味での「脱稿」の一歩手前です。

「じっくり読まなければ何となくいい感じ」が分水嶺

「じっくり読まなければ何となくいい感じ」ゾーンまでくると、文書の骨組みや各文に対する大幅な修正は起こりにくくなります。 したがって、「どこそこのタイポ修正」とか「この文を修辞的に書き換える」とか「この段落は内容が疑わしい」といった具合に、文書に存在する問題を局所化できる場合が多くなります。 「どの箇所にどんな修正がなぜ必要か」をかなり具体的に切り出せるようになるということです。

修正が必要な箇所と、その意図をはっきり区分できるのだから、修正の妥当性や方針をissueで議論したり、必要だと判断できた修正を切り分けたり、そういった作業が現実的になります。 「じっくり読まなければ何となくいい感じ」にまで到達した原稿に対しては、issueやブランチを切ったうえで、それに従ってコミットを細かく管理したりPull Requestを発行したりするワークフローが可能だともいえます。

問題なのは、まだ「じっくり読まなければ何となくいい感じ」に到達していない原稿です。 この段階になっていない文章を読むと、「どの箇所にどんな修正がなぜ必要か」をはっきりとは明言しにくいけれど、とにかく何かしら修正が必要であると感じます。 経験上、この段階の原稿が抱えている問題の多くは、切り分けが困難な問題です。 そのため、バージョン管理という視点で考えると、作業を分担したり作業内容に応じたコミットやブランチを駆使しようという努力があまり実を結ばない気がしています。

表面的な問題:統一感と文の品質

「内容をすべて書き出した」時点での原稿は、ソースのバージョン管理という観点で見ると、下記のような問題を抱えている状態です。

  • 統一感がない
  • 読みにくい文がある「ように感じる」

統一感がない

「内容をすべて書き出した」時点の原稿で用字用語がきれいに整っているケースはまずありません。 言葉遣いや文体、文章の温度もまちまちです。 なんとなく統一感がない状態です。

用字用語が揺れているだけなら機械的に対処できる面はかなりあるし、そのためのツールもいくつかあります。 とはいえ、「いう」と「言う」の使い分けみたいな、書き手がふだんあまり意識せずに書いている表現ほど、自動的な統一だと不自然な結果になったり、そもそも自動化しにくいように思います。 結局、けっこうなボリュームの本を編集するときは、頭から読みながら気になった時点でgrepして全体に修正をかける、といった作業をある時点で誰かがやっています。 そして、この作業は、たとえば用語ごとに独立したコミットとして切り分けるのが案外と困難なのです。 しかも、経験上は切り分ける意味もほとんどありません。

文章の統一感を上げる作業を、独立した細かいコミットとして切り分けにくい(切り分ける意味があまりない)理由としていちばん大きいのは、単一の行に複数の用語揺れが混在するケースが珍しくないからです。 「行う」と「行なう」の揺れを正す作業と、「無い」を「ない」に開く作業を別々の人がやれば、「行なわ無い」という非標準的な表記を「行わない」に直すだけであっさり衝突します。

それでもコミットを細かく分割すべきという考え方もあると思いますが、個人的な意見としては、 これはソフトウェア開発で「関数の仕様を変更したので関数名と引数リストを同時に修正しなければならないが、コミットとしては関数名に対する修正と引数リストに対する修正とで区分する」みたいな話に近いと思っています。 つまり実質的には分割の意味がほとんどない。

もう一つの理由は、他の修正作業との関係です。 ソフトウェア開発でも、バグ修正とコーディング規約に合わせる作業を同時にやれば、同一の行に対して何度も修正、加筆、削除、入れ替えが発生し、そのため競合が発生しやすくなるでしょう。 文章でも、統一感を上げる作業を文章・段落・節全体などの見直し作業と同時にやれば、解消に手間がかかる競合が多発します。 そのため、たとえば「用字用語の統一」のためのブランチを切って作業し、後でPull Requestとして一発でマージ、といった理想的に見えるワークフローは、他の作業をまじめにやっているケースほど非現実的だと考えています。 この作業は、ブルトーザーで荒地を耕すようなものだと割り切って、見つけた誰かがmasterに対して随時pushしていくほうが経験上はうまくいきます。

読みにくい文がある「ように感じる」

プログラムなら、コンパイルして型エラーが出たりテストに通らなかったりすれば問題の位置がかなり絞り込めます。 文章でも、文法上のミスや文章の品質については、指摘して問題箇所を特定してくれるツールがいくつかあります(textlintとかRedPenとかJustRight!とかMS Wordの校正ツールとか)。 とはいえ、人間が読むために書いているものをチェックしようとしている以上、結局は誰かが読んで初めて見えてくる問題がかなりあるわけです。 文意に曖昧さがないかとか、語調が不自然でないかとか、そういう問題は実際に読んでみなければ見えてきません。

こういった問題を修正しようとして、「この文を読みやすくした」とか「文意をはっきりさせた」といった名目で局所的なコミットをたくさん作ってしまうと、あとで面倒なことになります。 というのも、文章を読むことで初めて見つかるような問題というのは、実際には文章レベルより上位の段落や節が抱える問題の一部であることが多いからです。 このような問題に対する局所的な修正コミットや、それを意図したPull Requestは、後述する「段落や節に対する俯瞰的な修正」のための作業と競合してしまいます。

このような競合では、「段落や節に対する俯瞰的な修正」のほうが修正として適切なことが多いので、「局所的な修正」の作業は単純に水泡に帰すことになります。 「じっくり読まなければ何となくいい感じ」未満の原稿では、「局所的な修正」は「段落や節に対する俯瞰的な修正」と競合するものだと考えたほうがいいでしょう。 この段階の原稿に対しては、コミットを小さくすることが正義とは限らないともいえます。 「段落や節に対する俯瞰的な修正」を先にやることにして、気が付いた局所的な問題はissueなどの形でコメントを残すのみにしておくのが無難かもしれません。

根本的な対策:段落や節にまたがる俯瞰的な修正

コードが適切にモジュール化されているソフトウェア開発であれば、それほど変更が衝突することなく、個々のモジュールに対する作業を別々の人が同時にできるでしょう。 文章でモジュールに相当するのは段落ですが、段落は互いに密に結合しているし、抽象的なインタフェースもないので、互いの順序や文章表現について同時に配慮が必要です。 そのため、段落ごとに執筆や編集を分担するという方法は、あまりいい結果になりません。 とりあえずまとまった量を読んでみないと問題を切り分けられないし、切り分けている途中で問題が直せてしまったりするので、 判断を下しながらある程度の範囲を集中して読み進める、という工程が必要になります。

で、そういう不備を直していると、結果としてまったく別の問題が解消されたりすることがよくあります。 段落がまるまる修正されたので誤植が消滅したとか、段落を直した結果としていまいち曖昧だった文意がはっきりしたとか、 リライトが必要と思われた節とほぼ同じ内容の説明が他の場所にあったので節ごと不要になったとか。 しかし、誰かが該当する文章を局所的に修正していたり用字用語の統一を全体にわたってかけたりしていると、当然、その部分で競合が発生します。

俯瞰的な修正の作業は、かなり大きな範囲全体でコミットとして意味を持つので、他でコミットされたりPull Requestされた局所的な文章の修正をこのコミットの内容と調整するのはけっこう厄介です。 経験上うまくいくのは、たとえば「1章の見直し」といった作業内容ベースでブランチを切り、別のブランチでの1章に対する修正は用字用語の統一くらいに留めておく(この程度なら手動でマージできる)というワークフローです。 修正する内容に対するブランチを作るのではなく、修正する範囲を区切るためにブランチを作るイメージです。 このブランチでは、最終的にmasterへと自動マージできるような状態を維持しながら(masterの内容をときどきmergeしながら)、修正作業を続けるようにします。

執筆・編集にとって最適(いまのところ)だと思っているGitワークフロー

以上、だらだらと背景を書いてきましたが、結論です。

「内容をすべて書き出した」状態から「じっくり読まなければ何となくいい感じ」ゾーンにもっていくまでの間は、各コミットに必要以上に意味を持たせることはあきらめましょう。 完全に正史モデルを採用する必要はありませんが、この段階は開墾地をブルトーザーで整地するようなものです。 「木を抜く」作業と「穴を埋める」作業は並行してできるわけではないし、あとから一方の作業だけをやり直したいことも通常はありません (将来もし穴が必要になるとしたら、それは別の作業として考えるべきでしょう)。 それなりの規模の区画ごとに、全体の出来を眺めつつ、もくもくと均していくのが現実的なはずです。

一方、「じっくり読まなければ何となくいい感じ」ゾーンまで到達した原稿は、問題とその対処がコミットとして明確に切り分けられるので、修正の意図をはっきりさせた最低限のコミットを作り、それをmasterにマージするようにしましょう。

まとめると、次のようなポリシーで執筆・編集をするのがよさそうだというのが現時点での結論です。

  • 「じっくり読まなければ何となくいい感じ」まで達している原稿は、これから入れる修正の意図を明らかにしたいので、issueで議論してからコミットしたり、ブランチを切ってPull Requestにしたりする
  • それ以前の段階では、用字用語統一、誤記修正、タグ追加などの作業はブルドーザーによる整形作業だと割り切り、細かく管理するのはあきらめる。経験上、どのみちrevertやrebaseが不可能な作業なので徒労に終わる。信頼できる担当者どうしで作業をしているなら、見つけた人がmasterに随時pushするのが、他の作業との競合を回避するためにも効率的
  • それ以前の段階における局所的な文章の修正は、「じっくり読まなければ何となくいい感じ」ゾーンまではissueやコメントに留める。それ以降はPull Requestにする
  • それ以前の段階における節以上の範囲におよぶ内容上の修正は、作業範囲を隔離するためのブランチを切って作業する(このブランチではmasterにおける用字用語統一などの修正を適宜取り込みながら作業をする。Pull Requestにしてからマージしてもよいし、手動でマージしてもよい)

このほか、本文では言及しませんでしたが、下記のようなポリシーも経験上は有効であると考えています。

  • 脚注追加のように独立性が高いコミットとして修正を加えられる場合には、どの段階でもPull Requestにしてよい
  • メタ情報を、永遠にマージしないブランチで管理することもできる。索引タグなどは本文にマージする必要がないかもしれない

余談:「じっくり読まなければ何となくいい感じ」に到達したことを知るには

というわけで、文章のバージョン管理では「じっくり読まなければ何となくいい感じ」に至る前後のフェーズでコミットに対する考え方を変えよう、というのが現時点での自分の結論なのですが、 そもそも原稿の状態がこのティッピングポイントを越えたことはどうやって知ればいいのでしょうか?

残念ながら分かりやすい指標はまだ手にしていません。 しかし、循環論法めいていますが、ほぼすべてのコミットの意図を明確にできるようになった原稿は「じっくり読まなければ何となくいい感じ」になっているといえそうです。 その瞬間に気づくためには、いま施している修正が他の修正に依存していないかどうかを常に意識しながらコミットを刻む必要があるのかもしれません。