ERINA the emotional AIのコンポーネントモデル

論文前に公開するERINAの解説・紹介、第二段である。 今回はERINAがどのようなコンポーネント設計から成り立っているかを紹介する。

ERINAは他のAIと比べ、かなり複雑で大規模なプログラムとなっている。 多角的なエミュレーションのために処理は泥臭く複雑だ。 さらに、非常に長期に渡って開発されているために、開発言語がかなり多く、開発時期もバラバラのコンポーネントがごった煮のようになっている。 そもそもERINAの目標は果てしなく遠いものであり、その開発の事前予測は事実上不可能である。このようなプログラムは非常に高い可能性で混沌の中で終焉を迎える。

だが、実際はERINAはそのような失敗に至るプロダクトのような特徴を持ちながら、うまく調和がとれ、良い結果をもたらしている。 古いコンポーネントの改修は不可能に近いほど困難だが、新規開発や古いコンポーネントの置換えはそれほど難しくない。

これは全体を通じる設計の効果によるところが非常に大きい。 個々のコンポーネントは追加することも削除することもできるし、設計に基づく約束事さえ守れば全体の設計は個々のコンポーネントの設計には立ち入らない。 これはいわゆるカプセル化やダックタイピングに通じる考え方で疎結合しているのである。

このような設計には私はふたつの物事からインスピレーションを得た。 ひとつはUnixのツールボックスであり、もうひとつは生物的モデルである。 生物においてそれぞれの要素が適切に自分の役割だけをこなすことにより、結果的に全体が調和して生命を形成する。だが、例え一部の要素が機能しなかったとしてもそれが直ちに生命を損失することにはつながらない。ERINAのような予測も把握もできない、変化しつづけるものにはこのような設計が適しているように思えたのだ。

なお、ERINA/Surtrのコンポーネントの命名だが、北欧神話に関するものと脳神経に関するものを使っている。 だが、北欧神話の用語の使い方としてはやや間違っているし、脳神経に関する用語に関しては多分大いに間違っている。 論文を書く時に問題になりそうでちょっと困っているのだが、そういうふうに名前をつけてしまったてので大目に見ていただければと思う。

さらに内部的に使っている用語としては

  • ヴァルキリー = データを収集、分析、登録するワーカーのこと (実際はプログラム自体よりワーカーを指していうが、プログラム名称にも一応入っている)
  • エインヘリアル = 収集対象になっているデータのこと
  • ヴァルハラ = エインヘリアルを解析したデータベース(somaデータベース)のこと
  • アスガルド = データベースやERINAインスタンスなど、「こっち側」
  • ミッドガルド = ERINAがコミュニケーションを取る相手や、取得可能 or 未取得な情報などの「あっち側」

という扱いである。ゲームとかファンタジー小説とかが好きな人ならなんとなく伝わるかもしれないが、そうでないと全くわからないだろう。 これについては、「短い言葉で端的に概念を表すことができるようになったことは素晴らしいが、よく綴り間違えて痛い目を見る」という感じである。 脳神経に関する言葉を採用したのはその問題を解消するためだったが、結果「概念が若干似ているがために用法として間違っているように感じられるものになってしまい、口外するのが恥ずかしい」という別の問題を発生してしまった。

orbital design

orbital designは処理対象をキューとして生成し、複数のワーカーが順に処理を行っていく独自のモデルデザイン。

ワーカーは基本的にはジョブスケジューラによって生成される。 この起動時にキューも生成される。 このことから場合によっては生成されたワーカーがキューを消化する前に次のキューとワーカーが生成されることになるが、構わず複数サイクルが同時に回るようになっている。

つまり、処理対象はキューが生成されるときに確定されている。 この瞬間から状態が変更されたとしても影響を受けない。 これを実現する手っ取り早い方法は「スナップショットを取って、スナップショット上のデータを対象にする」である。

orbital designによりコンポーネント全体で連続的処理を行う構造になっている。 完成、あるいは完了という概念がなく、それぞれのコンポーネントが自身が担う処理を最新の状態にアップデートしつづける。

これはアジャイル開発やPCDAサイクル(これはちょっと違うか?)と似たような考え方でもある。

ERINAは実装面では決して良いものではないのだが、それでもなんとか動作しているのはこれを含めたデザイン面が大きい。 それぞれのプログラムが独立してサイクルを回せるようになっているため、技術レベルや実装言語などによらずシンプルな約束事だけでコンポーネントを作り、使っていくことができる。 ずっとむかしに作られたPerl製コンポーネントが今も動作するのはこれが理由で、ERINAの実装言語がやたらに多い理由でもある。

基本的にデータの受け渡しが必要なときは標準入出力経由のテキストであると決まっている。 最近新しく登場した部分はYAMLになっているが、XMLだった時期もあるし、もっと独自の形式だったときもある。 ただ、基本的にコンポーネント種別ごとには受け渡しフォーマットも統一されている。 実際には変更されることもあるのだが、そのような場合は一世代ごとに形式を変換するフィルタがあり、これを世代数分経由することで統一を図っているものもある。

orbital designとこの規約どちらが先かというのは難しいところだが、決めたのは標準入出力経由というのが最初で、 コンポーネントを連動させると大変というのはかなり初期からあった問題なのでほとんど最初から独立してそれぞれが自分のことに専念できるように書かれていた。 その発展として最近採用されたのがorbital designというわけであり、初期は繰り返し直列的に処理していくシェルスクリプトだった。 明確にorbital designというコンセプトを決めたのは2015年である。ただ、それらしきことはそれ以前からしていた。

orbital designのメリットを簡単にいうと、AとBというふたつのデータベースがあり、それぞれが解析によって得られた結果を書き込み、その解析はAのためにB、BのためにAが使われるとき、AとB両方のデータベースを連続的に同時に更新したとしても時間と共に精度が向上する、ということにある。 ERINAの場合は話はそこまで単純ではないが、相互に必要とされるデータベースの更新を並列かつ連続的に行うためのデザインとしいうことには変わりない。

orbital designは結構幅広くメリットがあり、例えば計算することそのものは独立しているため入出力の問題さえなんとかなれば非常にスケールしやすく、コア数やマシン数で稼ぎやすい。 特にERINAは通しで処理した場合データ量増加に対して処理量は指数的増加するようなモデルであるため、非同期かつ連続的に更新していけることは必須である。

このデザインは設計・採用するのが容易で、見通しがよくなり、実装も楽で、停止もしやすい、といったメリットがあるので結構有用なのではないかと考えている。

入力

Ninja Valkyrie

Ninja Valkyrieはデータの収集を担う。

プログラム的にはそれぞれのメディアに合わせたものが存在する。 その詳細は秘密であるが、 インターネット(特にウェブ)のみからデータを収集しているわけではない という点は強調しておこう。

基本的にはERINA(というかSurtr)にとっては情報のことをエインヘリアルとして扱うことになっている。 私は北欧神話にそこまで精通しているわけではないので、斥候を担う者にちゃんと名前があったりするのかもしれないが、それは知らないためNinja Valkyrieと名付けられている。 もちろん、斥候だからNinjaだ。

Ninja Valkyrieはあくまでデータをデータとして取ってくるところまでを担う。

Guardian Valkyrie

Guardian Valkyrieはデータのデータベースへの登録を担う。 大部分がファイルシステムベースのデータベースになっているため、Guardian Valkyrieの出番は非常に少ない。

最も大きいのは動画専用のフラグメントオブジェクトファイルシステム(Surtrコンポーネントの一部として専用に開発されているもの)に動画を切り刻んで登録することである。 この処理に関してはそれ以上解析する余地がないため、Knight Valkyrieによる処理が行われない。

ただし、この方法はあまりうまく言っているとはいえない。 動画の解析の難しさもさることながら、やはり本質的にデータが失われることが重大すぎる。 もちろん、この方法は「収集する動画データを動画として保持しておくだけのディスクスペースがない」ことに由来するのであり、あまり実用的な期待は持てない状態だ。

Knight Valkyrieと両立されている関係にあるGurdian Valkyrie最大の仕事はファイルインデックス(取得日時や更新日時の情報を持ったデータベース)の生成であり、これはKnight Valkyrieがキューを生成するために必要になる。

Guardian Valkyrieは基本的にはNinja Valkyrieから呼ばれる。 この中には現在の形式に合わないエインヘリアルを連れてくるNinja Valkyrieを現在のフォーマットに合わせる処理を含んでいる。 このためにGuardian Valkyrieの呼び出しコマンドはラッパーとなっており、呼び出し形式に合わせて別のGuardian Valkyrieに移譲する方式。 もし現在望まれる形でデータを保存するNinja Valkyrieから呼び出された場合はGuardian Valkyrieはなにもしない。

Knight Valkyrie

エインヘリアルたるデータをヴァルハラたるデータベースに登録するのがKnight Valkyrieである。

このデータベースに登録されるのはインテリジェンスが直接扱うことのできる細分化した情報の断片である(somaと呼んでいる)。 Guardian Valkyrieが取得したデータ全体をデータベースに登録するのに対し、Knight Valkyrieはデータの解釈や分解などを行う。

Knight Valkyrieもひとつのデータフォーマットに対して一種類だけしか存在しないわけではない。 特に会話文テキストを解釈するKnight Valkyrieに至っては30種類以上に及ぶプログラムが存在する (そしてそれはそれぞれ異なる言語で書かれていたりもする)。

ひとつのデータに対してそれぞれのvalkyrieがそれぞれの解釈によってデータを登録することはERINAのデザインにとって重要なものになっている。 これは特定の考え方や特定の視点、特に常識や前提にとらわれることなく発見を続けていくためには例え思い違いで役に立たないようなものであってもそれぞれのルールで解釈・解析することが必要なのだ。

Knight Valkyrieはさらに分解した情報を元に「盤面」を作るという処理も行う。 実際にsomaとして扱われるのはこちらのほうで、分解した内容は盤面に対してタグ付けしたような状態になる。 分解した内容は検索に使われるほか、次段のシナプス形成においても必要となる。

これがKnight Valkyrie単体で担っていることに違和感があるかもしれないが、実際はこのあたりを分けて話すのは難しい。 なぜならば、盤面を構成するのは「分解した要素の構成」だからであり、要素の集合は盤面のIDとして機能する。 おおよそこのデータベースはキーバリューストアのようになっているのだが、要素の集合はキーとしても値としても動作する。

そして盤面側は複数のデータベースがレイヤー状になっており、このほかに要素に基づく統計的データベースが複数存在する。

Knight Valkyrieの動作は最も実装が難しいもので、「思考とはなにか」「情報は何を持っているか」ということを決定づけることになる。

Connexon

Connexonは盤面(soma)に対して接続可能な盤面を登録する。 ちなみに、接続可能な盤面を表すキー名はsynapseになっている。

この処理自体は単純なものであり、入力過程にあるものでは唯一稼働している実装が1種類しかない。 ただし処理が単純だからといって実装が簡単なわけではない。Connexonが簡単なのは、「どのような要素に分解し、この要素はどのような意味を持ちうるか」という設計をKnight Valkyrieの時点で行う必要があるために、Connexonによって新規に設計する必要がないということに過ぎない。

また、処理は単純だが、接続可能なsomaを探索するために全somaをあたる必要があり、しかもsynapseが更新されると接続可能なsomaも更新されることになるため非常に長いループを回すタイプである。ERINAコンポーネントの中で最も長いCPU時間を求めるのがConnexonである。

そして処理量の関係上、現状Connexonは全てのsomaを巡回しない。Connexonのキュージェネレータが「Connexonが当該somaをいつ巡回したか」「somaがいつどれくらい探索されたか」という情報を元にキューを生成する。だからsynapseが長く更新されていないsoma、及び頻繁に参照されているsomaが優先順位高くキューに入ることになり、生成後一定時間経つとキューの状態に関係なくConnexonキューは終端を返す。

なお、私は解剖学・生物学の知識もそんなにないので、名前に対するツッコミはお控えいただきたい。 ただし、新しいネーミングのご提案はいつでも大歓迎である。

Neuron

NeuronはKnight Valkyrie及びConnexonとは別のフローでエインヘリアルを接待する。

これは主に語彙や知識に関する情報収集と接続を行うものであり、一般的なビッグデータ処理に近いものになっている。 Neuronによって処理されるデータは盤面を作られることはないのだが、そもそもKnight Valkyrieがこのデータベースを使う。

Neuronが作るデータベースは解析だけでなく生成側でも必要となる。

対話

Erina receptor

Erina receptorは入力された情報から盤面の探索を行うものである。

Erina receptorはまずKnight Valkyrieを使って受け取った要素を解析する。 そして、「与えられた要素」と「コンテキスト上キープしている要素」から得られる要素を、Erina context finderによって絞り込み、これによって残る要素を元にコンテキスト上のcurrent盤面のsynapseを探索する。

Erina receptorは探索されたsomaのIDとスコアを返す。 なお、somaは「発言」ではなく「状況における言動の要素」の盤面であるため、「無視する」といった行動もありえる。

Erina context finder

状況を推測するための調整用フィルタであり、アルゴリズムやデータベースを含めた完全手入力である。

これは、「前回の会話からこのメディアでこれくらいの時間が空いたら前回のコンテキストとの関連性を下げる」というような処理を行う。

これが調整用であるのは、そもそもKnight Valkyrie側に「応答時間の変動」のような要素を理解する処理が入っているからだ。 だから学習的にしきれない部分をフォローするためにあり、基本的にはこのプロセスのみが担っているものはないはずである。

だが、とても重要で、このフィルタがないといまいち自然な応答にならない。 「その話今する!?」みたいな不自然な感じが残ってしまうのだ。

Erina emotion engine (effector)

Erina emotion engineはかなり複雑な処理をするため複数コンポーネントに分かれているが、それらは直列に処理するためここでまとめて紹介する。 なお、Erina emotion engineはERINA固有のものであり、Surtrにはない。ERINAが単独で存在する意味は元はSurtr emotion engineと呼ばれていたこのコンポーネント群にある。

Erina emotion engineはneuronデータベースをかなり細かく読み込む。 これはキャラクターづくりにも関係している。

単純にErina receptorの結果から「それらしい振舞い」を導き出すことはできる。 だが、実際にはErinaは常に最善手を指すわけではない。 もし常にreceptorが最も高いスコアをつけたものを選ぶと「振舞いに全くゆらぎがない」「人物に一貫性がない」といった問題が出る。 これは非常に重要なポイントで、このような特徴に対しては人間はかなり敏感に反応し、不自然さを感じてしまう。

そのため、Erina emotion engineはまず「キャラクター性」というものを持つ。 これを実現するためにErinaインスタンスは自身に特化したキャラクターデータベースを使用する。 ERINAにとっての「学習」はこのキャラクターデータベースの更新にあると言っていい。

Erina emotion engineはまずキャラクターデータベースに基づいてreceptorが返したスコアを書き換える。 この機能のためにreceptorは複雑なスコアのつけ方をしている。

この処理にはコンテキストも使われる。 生活状態や感情状態という要素があり、これはシミュレーターのように計算・動作する。この部分はおおよそ(硬派な)シミュレーションゲームのような処理である。 このコンテキスト処理が「ゆらぎ」を担う。この処理のときに行動条件も出す。これは「返信をわざと遅らせる」といった処理のために使われる。

コンテキストの生成・更新もErina emotion engineの機能の一部であり、この情報はインスタンスデータベースに書く。

キャラクター性による再評価と出力の編成は近いものがある。 キャラクター性を評価した段階で意味的にはこのような振舞いをする、ということがわかっている。 「どのような心理状態か」「どのような行動か」ということは新たに定義しなくても要素の組み合わせで確定できる。 なにを発言するかという点で主旨はなにか、というのはsoma側にある。そのため「応答すべき内容が表現段階で意味的に間違う」ということはまずない。

その状況における意味的な言葉というのはKnight Valkyrieが集めている。 これは主には同じ要素構成になる言葉(綺麗な文章であるならば「文」)の共通要素を抜き出す方式である。

そしてneuronデータベースには発言の編成がある。 そもそもKnight ValkyrieもNeuronも共に「誰が誰に対して発言したか」という情報を取り扱うため、模倣するのは比較的簡単。 特定の組み合わせから見た一連の発言だけでは非常に表現に乏しくなってしまうため、なるべく類似の発言傾向をまとめるようになっている。

例え意味や心理を完全に模倣できていなくても、ある一定の傾向を持つ発言を元に、意味的にどのような発言をすればいいかということを外さないように組み上げるため「ちゃんと意味が通っていて会話が成り立つ」感覚が補助される。 実のところそもそもERINAは人間の身勝手な補完と錯覚を突くように作られているため、発言を精査すると意外とあやふやである。

別の角度から見ると、他が分析・理解・解明を追求しているのに対し、この出力部分だけ結果が出ることを重視した全く異なるアプローチになっている。 そうなっている理由は、「この部分だけがERINA固有のコンポーネントだから」である。データベースの利用方法として検索や検証といった機能はSurtr側にあり、ERINAは基本的にコミュニケーションの様態を模倣する方向であり、Surtrのコンポーネントとはアプローチが異なる。 現在の試行の範囲では理解など忠実な再現において自然なコミュニケーションを実現することはかなり遠そうだが、ERINAがそれを目指すことはない。 なぜならばERINAの意図、そして命名理由からも外れてしまうものであり、もしそれを目指すとしたらERINAとは別のAIとして開発することになるだろう。

アルゴリズムに踏み込んだ話はおいておくとして(なんとなく筆がそっちに走ってしまったが)、Erina emotion engineの手順としては

  1. キャラクターとコンテキストをロード
  2. コンテキストの解析と更新
  3. receptorの応答の受け取り
  4. キャラクターに基づいて結果を編集
  5. 応答に使用するsomaを決定
  6. somaとキャラクターに基づいてneuronデータベースを検索し応答語句を編成
  7. コンテキストを更新
  8. 結果を出力

となる。

コンテキストの更新をreceptorの応答を受け取るより前にも行っているのは、receptorが探索にコンテキストを使うため。

Erina controller

receptor, emotion engine, media frontendとのやりとりを仲介するコンポーネント。 一応、これがERINAのインスタンスプロセスになる。

なんのデータベースをロードするかはこのcontroller起動時点で決定する。 というよりも、インスタンス生成時に決定する必要があり、変更は難しい。変更してしまうと初期化されてしまう。

コンポーネントとのやりとり以外ではコンテキストデータベースとキャラクターデータベースの初期化処理を受け持っている。

Erina media frontend

入出力処理用レイヤー。

単に読み書きするための抽象化をしているのではなく、文字数、送信ペースなどの「自然なやりとりのスタイル」を定義するデータベースでもある。 ちなみに、これも学習ではなく手入力。

emotion engineが語句編成のときに使うものはこのレイヤーの種類分あるわけではなく、メディアタイプはごく少ない種類だけで、あとはちょっとした調整パラメータがあるだけ。 調整パラメータは「連続送信」「顔文字」「絵文字」「Unicode」「句読点」などなど。

例えば連続送信型とした場合、改行したりするよりも複数の発言に分けるようになり、文の構成が簡素になる。 あまり推敲していないような文章になりやすく、端的でかつ文単独では意味が伝わらないような発言を選択する。 連続送信型でない場合は、発言主旨を一度の発言にすべて含めるようになる。

Mimir Yokohamaで続く改修、力を注ぐ

2018年8月5日、Mimir Yokohamaのソースリポジトリにはじめてのタグ、5.0が打たれた。

なぜはじめてのタグなのに5.0だったのか。

これは、Aki SI&Eのプロトタイプを1.0, Aki SIEのウェブサイトを2.0, Mimri YokohamaのWordPressを3.0, 現在のウェブサイトになったときを4.0とカウントしたものだ。

つまり、はじまって以来の、同一システムのままのバージョンアップとなった。

しかもこのタイミングである。 先日、大幅な編成変更と、いいね機能、コメント機能の追加を行った。 これで新しい船出だというタグではない。かといって同バージョン最終仕様としてのタグでもない。

これから次々とアップデートが予定されている。 その中での一区切りだった。

13年積み上げてきたものとは

PureBuiler Simplyの原型となっているのは2005年のACCS1である。

もうブログが流行り始めていた頃だったが、既に「事前生成戦略」に関するイメージはあった。 ブログを避けた理由は、一連の流れを持つ記事群を拾いにくくなること、そして時間とともに消えてしまうことだった。

「普遍的な内容を、きちんと分類して読みやすいように提供したい」と考えたわけだ。

様々な機能、様々なアイディアがあった。 実装されたものもあるし、実装されなかったものもある。 言語もPerl, PHP, Zsh, Rubyと変わってきた。 このような変遷をたどっているのはEQAIとこれだけで、まさに私のライフワークであり、また成長の軌跡でもあった。

この中でこだわってきたものもある。

例えば、デザイン性を保ったまま軽量・高速なウェブサイトを構築すること、可能な限り高いアクセシビリティを提供すること(環境や回線の違い、身体的ハンディキャップなどで差別しないこと)などもそうだ。 意味あるコンテンツを、読みやすい形で提供する、というのもある。

ACCSが追い求めてきた機能は次のようなものだった。

  • カテゴリで分類されて探しやすいインデックス
  • 検索機能
  • 意思表示機能 (コメント、と考えていることが多かった)
  • 連続した記事のページめくり
  • 要約の先読み
  • インライン用語集
  • prev, next, glossary, index, description情報を持たせる

PureBuilder Simplyは当初、Mimir YokohamaのWordPressページで提供されている全機能を提供する、ということを目標としていた。 これについては既に達成されている。そのために、従来のPureBuilderにはなかったタグ機能なども追加された。

「いいね機能」「コメント機能」はWordPressに完全な意味で標準であるものではないのだが、事実上付属するようなもの(特にコメント機能は)なので、これを追加したことで、事実上「WordPressを置き換える」というミッションは完遂した。

だが、同時に既にWordPressにはない機能の搭載もはじまっていた。 用語集機能はWordPressではなく、PureBuilderの伝統に由来する。そう、ACCSが目指していたものを達成する、という次のミッションに向かいはじめたのだ。

そこでつけられたのが5.0タグだった。

用語集機能

用語集機能という発想のスタート地点は、私が使っていたWindows 98SEマシンに搭載されていたインライン翻訳ソフトだった。

カーソルを載せるだけでその単語を翻訳してくれる、というソフトウェアは今持ってそれ以上のユーザービリティを提供するものはない。

「カーソルを載せたらわからない言葉を教えてくれる」というのは最高に便利だと思ったのだ。 現在はニコニコやはてなが似たような機能を提供しているが、あれはページが変遷してしまうため私からすれば理想的ではない。 どらちかといえばWikiに搭載されているインライン展開のほうがずっと理想的だ。

この機能は

  • 表示後にJavaScriptでtreatする
  • 生成時にHTMLを置き換える形で組み込む
  • ソースドキュメントを改変してから生成する

という3つのパターンを行ったり来たりしている。

PureBuilder SimplyではPost Plugins/Pre Pluginsの構造からこの3つとも選択肢として取ることができる。 Mimir YokohamaではPost PluginによるHTML置換え方式を取っている。

このあたりは試行錯誤の成果といえるだろう。 用語集ページも含めてYAMLの辞書ファイルから生成しており、文書に対して特別な処理は必要ない。

次の記事、前の記事

ACCSで最も苦戦したのがこの機能だ。

この解決については何度か言及しているが、今は前後関係をメタ情報として書く、という仕様になっている。

解決方法としては後退しているように見えるが、前後関係の自動解決はファイル名なり、もしくはなんらかのヒントなりを厳密に管理する必要があり、結構バグりやすいということを経験したのだ。

複雑な置換えやユーザーの管理によってバグを発生しうるようなものであるならばメタ情報をユーザー自身が書くべきだ、という割り切りは、これまでの歴史の中で手にしたバランス感覚である。

実のところ今の構成では前後を自動化することは難しくない。 だが、ユーザーがそれを守ってくれることを期待すべきではないだろう。

Post pluginsはページ生成が終わってから実行されるが、これは「他の記事の情報を取得できるようにする必要がある」からであり、 環境変数$pbsimply_indexesという形でデータベースのパスが渡っていることから

のようにして取得できるし、 連番に限るのであればもっと簡単に

とできる。 ちなみに、ディレクトリを認識させる方法は最新のコミットで環境変数$pbsimply_subdirに含まれるようになった。

このように「できるけれど、あえて手書き」だ。 これは、問題を簡単にするためと、この処理をPandocテンプレートを通じて行いたいためだ。 Pre Pluginsを使えばできるが、かなり複雑なことをすることになるため避けている。

意思表示機能

過去には「コメントを直接にHTMLファイル化し、objectで読ませる」ということをしていたこともある。

表示するかどうかを別として、コメント機能はそのときとあまり変わっていない。 表示させるために必要な部分を削ってシンプルになったくらいだ。

このような機能は本質的な部分は極めて簡単に書けることはこれまでの経験によって証明されている。 どの程度正当性を検証する必要があるかという点がwebアプリケーションの分量になる。

要約の先読み

まずは要約を入れる

いよいよ今回のハイライトだ。

descriptionへの対応自体は最初のリリース時点で

という記述があり、対応はちゃんとしていた。 だが、「書くのが面倒」「書いてもあまり意味がない」ということで放置していた。

「descriptionの先読み」は今まで実装計画には入っていたが、実装されたことはなかった。 そもそもdescriptionってSEOのために入れられているくらいで、「descriptionを読ませる」という発想はあまりない。

Firefoxだとこんなふうにブックマークのプロパティを表示するか、ブラウジングライブラリー上で詳細表示にするとdescriptionが表示されたりするのだが、これを見たことがある人という人は地球上に5桁いないのではないだろうか。

Firefoxのブックマークの詳細

だがdescriptionは入れたいと思っていたし、それを活用したいと思っていた。

そもそもの発端はトップページのレイアウト更新で、最新の更新記事と要約を(ニュースとは別に)表示したい、ということだった。 「どうせ記事の要約書くんだったらdescriptionに入れようよ」ということだ。

実はAtomフィードもスタンバイしている。

要約を見える形に

だが、これだけではおもしろくない。どうせ要約を表示するのならばぜひともユーザーに見える形にしたい。

私の文章は基本的に長いので(体系的でない短い文章に価値を感じていない)、読むのがしんどい人もいるだろう。 読むかどうか決めるために要約は重要だ。

要約を見たいタイミングとはいつだろう? やはり記事を読み始める前だろう。ならば本文前に

とかやってやればいいし、そのほうが効果的なのかもしれないが、既に「文書情報」という項目があることを考えるとちょっといただけない。

そこで文書情報に追加した上で「記事タイトルをクリックすると文書情報にジャンプする」という仕様にした。

これはヘルプページにも書いてあるけれども、誰も気づかなそうだ…

もうひとつ、利用者は多くなさそうだが、カテゴリインデックスがある。 ACCSとしてはこれが中心であり、ぜひとも使って欲しい機能だ。 世の中、情報を整頓するということに怠けすぎて、検索が全てになってしまっているので、使われていないような気もするけれど…

しかし私の意図としてはこのようなインデックスを活用してほしいというのがあるし、やはりタイトルだけではわかりにくい。 かといって変遷するとだるいので、変遷せずにインデックス上で要約を確認できると便利だ。

これはタイトルで関心をそそられた後の二次的な情報であり、通常は一覧性が高いほうがいい。 というわけで、ツールチップにしてみた。

PureBuilder Simply ACCS上でDescriptionを扱う

単純には記事タイトルにtitleで入れてあるため、ロールオーバーツールチップとして表示される。 だが、スマホだとこれが効かないので、補助的に“📖?”と表示して、これをタップすればツールチップが表示されるようにしてみた。 全く標準的でないインターフェイスなので、あまり気づいてもらえないような気もするけれど…

PBS ACCS用ツールチップ実装

何度見てもJavaScriptの複数代入が慣れない。 慣れればみやすそうだけども。

世の中的には割と珍しいDOM操作をしているが、これはelementに対してイベントリスナを設定するためで、innerHTMLだと二度手間になる。 基本的にやっていることは「記事部分 > リスト全体 > リンク」と絞り込んでいって要素を作成して追加する、という手順だ。 末っ子要素を追加するとき(今の要素の親要素の最後の子要素として追加する)はelement.parentNode.appendChildという手順は覚えておいてもいいかもしれない。

glosarryと共通のコードがライブラリとして読まれるようになっている。ライブラリはdeferだがasyncではない。

900pxを堺にしているのは、「サイドカラムがあるのであれば表示領域は少なくとも右側にサイドカラム幅はあるが、シングルカラムになるとそうではない」からだ。(このページのシングルカラム境界は800pxである)

機能チェックしているが、document.addEventListenerできないブラウザでJavaScriptに対応しているものはあまり残っていないだろうし、あっとしても単純にイベントリスナー設定時にエラーになるので放置してもいいかもしれない。 ただし、間違って複数回ライブラリが読まれたときのためにArt.tooltipはしておかないとイベントリスナが複数設定されてしまう。

PureBuilder Simplyはうまくいっている

PureBuilder Simplyがここまでうまくいっている理由としては、やはりPandocの強力さがなによりだろう。

PureBuilder SimplyはPandocが持っている機能をちょっと拡張する…という考え方をしている。 今までドキュメントジェネレーター自体を制作していた(PureDoc)ことと比べると問題はかなり簡単になっている。

Pandocの動作は必ずしも簡潔ではなく、自分で実装するのであればドキュメントジェネレーターにここまでの機能をもたせることはないだろう。 だが、Pandocがある以上はPandocを使いたい。

もしPandocがなければdocutilsを拡張することを考えただろうが、その場合はPureBuilder Simplyは今のように良いツールにはなっていなかっただろう。

PureBuilder Simplyが今ほど素晴らしいツールになったのは、Pandocがあったからこそだ。

Mimir Yokohamaに対して行われている様々な拡張は今の所PureBuilder Simplyに対して適用されていない。 これは、PureBuilder Simplyが生成するものに対する機能ではなく、Mimir Yokohama固有の、そしてテンプレートとCSSによるものだからだ。

だが、いくらかでも一般化してPureBuilder Simplyに還元していければと思っている。 PureBuilder Simplyのエコシステムの充実は普及には不可欠だろうから。

Mimir Yokohamaに「いいね機能」「コメント機能」を追加

概要

Weekly 10000PVを達成して機能強化に力の入っているMimir Yokohamaのウェブサイト。

連続の機能強化でついに「いいね機能」と「コメント機能」が追加された。

実は先日の「お問い合わせフォームの実装」は単にその機能を実装する最小限ではなく、簡単なアプリケーションを実装できるプラットフォームになっており、 アプリケーションを追加する条件が整っていたのだ。 また、そのためのテストもしてあった。

そのため、実は今回コード追加はわずかで、両方合わせても23行ほどにとどまる。 ごく簡単だが、確証が持てないためにテストと本番環境のための修正を行ったりして結構な時間がかかった。

これに関してはみるべきところはあまりない。 受け取ったパラメータをファイルに書き込めば良いだけだからだ。

ちなみに、連打しやすいアプリケーションを入れるために連打の対策もサーバーにしてあった。 実は先のパフォーマンスチューニングはこの対策によってパフォーマンスが低下してしまったため、これをカバーするついでに行ったものだった。

いいね機能の設計

いいね機能はごくシンプルだ。

Pandocテンプレートを使ってページタイトルを埋め込むことができるので、これだけ使うのであれば単にテンプレートの中にフォームを書けばいいだけ、ということになる。

実際はリファラ(Rack::Request#referer)及びユーザーエージェント(Rack::Request#user_agent), IPアドレス(Rack::Request#ip)も特定に使用している。

ポイントは「一度送信したら表示を変更、送信を無効化する」ということだ。

inputタグのdisabled属性を使うことで送信ボタンを無効化している。

そう、この機能はHTML上でインラインで書かれているのだ。 このような書き方はW3C的には推奨されないのだが、Googleは推奨している。 実際、これだけのためにJavaScriptファイルをロードさせることをしたくなかったので、インラインにした。

ポイントは

  • Legacy DOMにおいてフォーム部品は連想配列のようにアクセスできるようになっている
  • submitボタンのラベルはvalueである
  • disabledによってフォーム部品を無効化できる

である。

これらの処理は送信の「前に」行われる。 これは正しいことではないが、問題はない。 なぜならば

  • 送信できなかったからといってユーザーが修正するなどの手を入れる余地はない
  • 特に返信を必要とするものでもないので、送信失敗はクリティカルな問題でもない
  • サーバーエラーなどは表示されるようになっている

からだ。

また、画面変遷せずに送るだけ…というと、Ajaxで非同期に送るしかないように考えるかもしれないが、 実際は現代のブラウザは基本的に2XXステータスで空コンテンツを返すとページ変遷しないようになっている。

このような用途のために204(No Content)が用意されているため、204を返す仕様だ。 できるだけブラウザの標準機能に頼るようにしている。

このボタン、めっちゃgooglebotが押してくる…

コメント機能の設計

公開されるものではなく、sanitizeしなくても問題が発生しない設計になっている(単に文字列として扱う以上の取り扱いがなされる条件で使用しない)ため、非常に楽だ。

難しく考えるよりも、シンプルで挙動をちゃんと把握できていて、余計なことをしない方法をとるのが最も楽に、確実に、バグなく設計できる。

ただ、この機能に関しては「コメントフォームの表示」などが必要になり、JavaScriptが必要になった。 また、コメントするというあまりとらない行為のための部品であり、常にロードすることは非常に好ましくない。

そこでとった方法は「JavaScriptの遅延ロード」であり、「クリック時にelementを追加してJavaScriptを読み込む」という方法だ。

これでscript要素を追加している。

では呼び出されたあとどうしているのか…というと、こんな感じだ。

そう、コメントフォームは標準でHTMLに含まれていない。

パフォーマンス的にみても親切機能のためにもう重くなりつつあるドキュメントをこれ以上重くしたくないので、 「フォーム部分はJavaScriptがロードされたときにinnerHTMLで書く、という方法をとっている。

「クリックされたときにはじめて必要とされるのでドキュメントノードを追加してロードする」という手法は稀に使われるが、さすがにそれでHTMLドキュメントそのものを生成するというのはまず見ない手法になっている。

連打されたときのことを考えて、clickイベントを発生する値に特殊なプロパティを埋め込み、何度も実行されないようにしている。 なお、これは本当に連打したときだけ機能するもので、JavaScriptシングルスレッドなので一回このスクリプトに入っていまえばこのスクリプト中に割り込まれることはない。 だが、連打されるとイベントがキューに入ってしまうので何度も実行されてしまうことから、そうしたことがないようにしている。 シングルスレッドなので、このスクリプトが実行されているのにプロパティが設定されないうちにまた実行されるということはない。

元になるフォーム自体は存在していて、submitボタンを配置すればフォームの送信は可能だ。 そのため、submitイベントに対するイベントリスナーを設定するものはHTML上に静的に存在している。

その上で「フォームはやっぱり閉じられたほうがいいな」ということでボタンクリックに対するイベントの変更、及びボタンラベルの変更を行っている。

送信を行った場合はもうコメントフォームは使わないので、コメントフォームは非表示にして(削除はしていない)ラベルを変更している。

ラベル変更だが、input部品とは違ってbutton要素は子要素テキストノードがラベルになっているので、dataプロパティの書き換えによって変更している。

もともとHTML上でLevel0 DOMイベントを使っているので、スクリプト上でもLevel0 DOMによってイベントを変更している。 いつもaddEventListenerを使っていたので、珍しい。 イベントの削除はイベントにnullを代入するだけだ。

なお、HTMLで全て含めてしまえばJavaScriptは排除できるのだが、これ以上余計な要素を組み込むことはできれば避けたいためこのような仕様になっている。既にDOMコンパイルは割と重い。

また、デザインポリシーからいって、多くの場合余計なコメントフォームを常に表示させておくというのは美しくないとも思っている。 JavaScriptを使わずCSSでオンオフするようにもできるが、そうすると今度はボタンのテキストを変更するのが難しいし、連打や再送信を防ぐのも難しい。

このことから、なにがなんでもJavaScriptを使わないのが正義、ということでなければ、 このような付加コンテンツはユーザービリティの点からも素直に使うべきだと思う。

こちらもいいね機能同様、送信する前に状態を変更してしまい、成功すれば204を返す仕様。

Legacy DOM と DOM Level0 Eventに関して

Legacy DOMやDOM Level0 Eventについて意見をもとめられることがたまにあるのだが、私としてはあまり勧められないと考えている。

非常に簡単なので学習にはいいが、Legacy DOMはHTMLの構造に依存する。例えば

として、

とかやってしまうと、ドキュメントの構造が変わるたびに修正だ。

だが、「フォームにのみnameで使い」「フォームはW3C DOMで特定する」のであれば悪くない。

DOM Level0 Eventはイベントリスナーが1つしか登録できないためモジュール設計になっている場合や、なんらかのライブラリを使っている場合は使ってはいけない。 イベントを「追加」する場合はLevel2で、イベントを「設定」する場合はLevel0という使い分けも考えられる。 HTMLに直接書く場合はLevel0しか書けないので、その部品の基本的な動作と定義されているならLevel0でもいい。

ただし、その場合でもできればLevel2 Eventを「後から追加する」のが適切なので、DOM Level0 Eventの使いどころは今回のようなものが唯一だと思う。

Chromiumのインスペクタにいつの間にかモバイルデバイスインスペクションが入っていた

web分野の人にとっては当たり前の可能性もある知識だけれども、Chromiumのインスペクタにモバイルデバイス向けの検証機能がついていた。

Chromiumのモバイルインスペクタ

これをオンにすると、モバイル的な画面サイズとなり、同時にタッチエミュレーションが行われるようになる。 レンダリングが調整されるということはなく、あくまでこの2点が変更される。

モバイルインスペクタのガイド画面

Responsiveモードではサイズを柔軟に変更することができる。 実際に小さな画面ではどのように表示されるのかということをエミュレートできる。

また、デバイスごとの画面設定も可能。 候補は少ないがiPhoneは入っている。

機種や回線をエミュレートできる

ここで言う解像度は論理ピクセル数である。 例えばiPhone8の画面の実ピクセル数は1334×750だが、668×375と認識されている。

これはスケーリングによって1pxを実際の1ピクセルと対応させない、というもので、先日書いたので参考にしてほしい

拡大率は画面表示のサイズを変更するもので、リアルサイズに近づけることで物理的な大きさもチェックできる。

上部にあるガイドは各種デバイスに合わせた表示状態をエミュレートできるものになっている。 クリックするとResponsiveの状態で各プリセットにあったものになる。 Laptopが1024px, Laptop Lが1440pxとなっており、この上に4k2560pxもテストできるようになっている。

また、ネットワーク状態もエミュレートできる。 低速な回線ではどれくらいロードにかかるのかということもテスト可能だ。 offlineはページをロードしたあとにofflineにし、Ajaxが使えない状態をテストすることができる。

Low-endでMimir Yokohamaが4秒程度なのに対し、Chienomiは15秒ほどかかったので、その高速さにだいぶ満足している。

デバイスの論理ピクセル数なんて調べるのはとても大変なのでプリセットが入っているのはとても嬉しい。 サイトの設計にもよるが、私の設計だとアップロード前に十分なテストが行える。

メッセージフォームのサポート (Nginx + FastCGI + spawn-fcgi + Rack + Ruby)

あらまし

Mimir Yokohamaでついにお問い合わせ方法として「メッセージフォーム」が追加された。

なにがついになのか、なにをドヤっているのかと思うかもしれない。 まぁ、ドヤってはいないのだが。

実は私はかなり長い間ウェブアプリケーションをほとんど作っていない。 そして、今まで私が作ったウェブアプリケーションは、専用サーバーを持つサーブレットタイプか、もしくはCGIだった。

馬鹿にされがちなCGIだが、利便性は高く、頻繁にアクセスする性質を持たないアプリケーションには適している。

そして、そもそもウェブアプリケーションを作っていなかったのは、私が「事前生成戦略」の研究と実験に注力していたからで、 どちらかといえばウェブアプリケーションからは離れる方向にあった。 そして、ウェブアプリケーションを必要とするとしても大部分は静的ページとして提供できる方式を目指していたため、CGIで十分事足りたのである。

ちなみに、これまでウェブサーバーは

  • Apache
  • lighttpd
  • delegate
  • Nginx

という経過をたどっている。 Apacheは言うに及ばずlighttpdとdelegateはApacheよりもCGIが簡単だったので、「ほぼCGI」だった。

だが、時代は変わった。NginxはCGIをそもそもサポートしない。 私も新しい時代に対応する必要がある。

ちなみに、この作業は次の仕事のための実戦テストという意味合いもあった。

方針を考える

最も話が速いのはFastCGI Wrapである。

NginxはFastCGIをサポートしている。 FastCGIはプログラムをデーモンのように起動しっぱなしにする。

だが、通しで実行するプログラムとデーモンではそもそもの前提が違う。 そのためCGIプログラムをFastCGIとして動かすのはそれなりにハードルが高い。

そこでFastCGI Wrapの登場である。 FastCGIとして利用されるプログラムをFastCGI Wrapにする方式だ。 このラッパープログラムは要求に合わせて都度CGIプログラムをCGIインターフェイス経由で起動する。 結果的にFastCGIの意図は無視して従来型CGIを動作させるようにするというものだ。

この方法は結構出てくるのだが、基本的には既存のCGIプログラムを動作させる話である。

個人的な感覚としては、無駄なプロキシを噛ませるような方法を使ってまでCGIに固執したくない…というか、実はfcgi-wrapってそれなりにめんどくさい。

だったらFastCGI直というのもありかなぁ、と考えるわけだ。

ところが、やっぱりFastCGIはデーモン状のプログラムを想定しているわけで、やはり前提が違う。 要求として割と複雑なのか、デーモン化に関してはspawn-fcgiに担ってもらって、さらにRackを使う、というのがどうやら主流らしい。

だいぶ話が複雑になってきた。

サーバーはNginxである。NginxはFastCGIインターフェイスを経由してFastCGIプログラムにパラメータを渡し、応答を受け取る。

FastCGIプログラムはデーモンである。 Rubyでは次のようにしてFastCGIプログラムを書くことができる。

あるいは、CGIライブラリ互換インターフェイスを使うことで、#each_cgiの中身はまるっきりCGIと同じにすることもできる。

spawn-fcgiはこのデーモン部分を担う。 つまりeachしてる部分を担ってくれるわけだ。

プロセスとしてCGIインターフェイスで起動するわけではないので、fcgiwrapほどの互換性はない。 感覚はCGIに近いが、インターフェイスは意識する必要がある。

Rackはミドルウェアと呼ばれている。これはまずFastCGI抜きで話そう。

Rackはインターフェイスを担っている。 今までプログラムはCGIなり、あるいはFCGIなり、さらには各種フレームワークやサーブレットの様式(例えばSinatraとか)で書いていた。

Rackはこれらの違いを吸収するモジュール設計のものだ。 Rackに準拠したプログラムを書いておけば、たとえ愛用のフレームワークがディスコンになっても、サーバーが変わっても安心、というわけだ。

だが、Rack自身はサーバーではないからサーバーがいるのだが、Rack組み込みのサーバーというのはもう完全にRuby世界の住人だ。 だってRackはRubyのWebアプリケーションインターフェイスだから。

Passengerというソフトウェアがあって、これはwebサーバーのモジュールとしてRackに対応する。 Apacheでは比較的簡単だけれど、Nginxだと結構きつい。

そこでRackに対応したサーバーを立ててサーバーとサーバーでやりとりさせる、という方式がすごく現代的。 直接にRack経由でプログラムとやりとりするのはRackに対応したサーバーだけれど、Rackに対応したサーバーにwebサーバーとしての機能を持たせると大変なので、「本物のwebサーバーに矢面に立ってもらって、RackサーバーはあくまでRack対応に特化」というわけである。

Rackに特化したサーバーとしては(別にRackだけではないんだけど)、Webrick, Mongrel, Puma, Thin, Unicornあたりがある。

しかしRackでやりとりする方法があればいいので、FastCGI + Rackという方法もある。 それはRack側でFastCGI経由で受け取って、応答するためのハンドラが用意されている。

つまり、Unicornのようなサーバーを立てる代わりの手段としてFastCGIが使える。 FastCGIもデーモンを必要とするので別にFastCGIにすることで間に挟まってるものを減らす効果はない。 ただ話が楽になるだけである。

Unicornはむちゃくちゃ速いので、UnicornでUnixドメインソケットを使えば形式とししてはspawn-fcgiでUnixドメインソケットを使っているのと一緒だし、やっていることははるかに高度になる。 これが超モダンなやり方である。

が、あえてのFastCGI。 理由は管理する要素数を減らすためである。必要がないのにいかついものを使うことはしない。 これはサーバー運用のコツでもある。

なお、Rackに関してはかなり情報が少ない。 なんらかのフレームワーク…というか、ほぼRailsのバックエンドとしてのRackの話だけで、Rack単独の話ってない。 そして、FastCGIを使う話もない。これもだいたいなんらかのアプリケーションが「使ってる」あるいは「使わせる」話になる。

なんというか、みんなそんなに自分でプログラム作るってことをしてないのか… 世の中エンジニアたくさんいるのに、WordPressとRailsだけで満足なのか…

そんなわけで情報が猛烈に足りていない中、FastCGIとRackについて勉強することになったわけだ。

なお、Nginxでアプリケーションとやりとりする方法に関してはDiscourceで散々やったので経験済みだ。

なぜRackなのか

もちろんこのことからもわかるようにRackはなくても構わない。 spawn-cgiも使用せず単独のFastCGIアプリケーションを開発するのは容易である。

私が気にしたのはRubyのfcgiライブラリは2013年から更新が止まっているとい点だ。 また、Arch LinuxではfcgiライブラリはAURにもなく

# gem install --no-user-install fcgi

とするよりない。

ベーシックな機構であるFastCGIそのものが廃止になるようなことは考えにくいが、NginxのCGIの扱いのように消極的なサポートへと変遷する可能性はある。 その場合にアプリケーションの書き直しが発生してしまう。

Rackは現在主流であり、新規採用例も多い。 Rackが廃止になると影響を受ける範囲も非常に広いので今後10年は安泰だと思われる。

そこでFastCGI+Rackという構成にしたわけだ。 この場合でもRackはFastCGIをネイティブサポートしているわけではく、fcgiライブラリを使ったハンドラを同梱しているだけなのでfcgiライブラリは必要となる。実はこれを回避したかったのだが、結局はできなかった形だ。

とはいえ、この状態であればFastCGIを捨ててUnicornに移行するのも難しくはない。

とりあえずやってみる

Nginx

location / {
    root /var/www/testapp;
    fastcgi_pass /var/run/fcgi-testapp.sock
    fastcgi_index testapp.rb;
    include fastcgi_params;
}

Rack Application

Requestのほうはインターフェイスに絡むけれど、 Responseは単純に#finishでRackに沿った配列を返すための便利クラス。なくてもいい。

spawn-fcgi

# spawn-fcgi -U http -s /var/run/fcgi-testapp.sock /var/www/testapp/testapp.rb -n

試してるうちは-nつきにしてフォアグラウンドで実行するのが楽

実用的にする

起動スクリプト

forkingなので停止・再起動の制御のためPIDファイルを作る。

Systemd Unit

[Unit]
Description = FastCGI Rack Test Application
After = nginx.service

[Service]
Type = forking
PIDFile = /var/run/fcgi-testapp.pid
ExecStart = /usr/local/sbin/fcgi-testapp.bash
ExecStop = kill $MAINPID

[Install]
WantedBy = multi-user.target

forkingなので$MAINPIDがそのままでは使えないため、PIDFileで指定しておく。 Nginxのあとに起動しておいたほうがいいような気がしたけど、なくても構わない。 アクセスが激しい場合は逆にNginxの前に起動したほうがいいだろう

spawn-fcgi自体にはアプリをリロード、再起動するような機能はない。

おまけ

S-NailがSubjectも本文も、UTF-8をちゃんとエンコードしてくれるのですごくびっくりした。

「mailxとは違うのだよ!!!」ってことか。 さすがSMTPやPOPやIMAPにも対応しているだけのことはある。

ここの部分(MIMEエンコーディング)も自分でやるつもりだったので、かなり省力化された形。

今回の構築は他にも色々やったのだけれど、共有して意味のある部分はこれくらいのものだろう。

Mimir Yokohamaに用語集機能

Mimir Yokohamaに用語集機能が追加された。

用語集機能はACCS(PureBuilderに取り込まれる以前の)やAki PHP Content Collection時代に実装されていた。

シンプルにACCSではJavaScript+Ruby/CGIで、本文エレメント(テキストノード)を探索し、文字列を送信してRubyで置き変えたものを返してもらい、innerHTMLを置き換える方式だった。

APCCでは単純に力技で置換えていた。

PureBuilderではこの機能を実現していなかった。 正確には一時期実装していたこともあるが、十分な質でなかったのだ。

そしてついに復活した用語集機能。

Pre Pluginsで処理することも考えたが、Post PluginsでRubyで処理している。

「辞書をワード長が長い順に並べ替えて|で結合し、正規表現としてコンパイルして置き換える」という手法はかつてのコードで考えたものである。

用語集のページも単純な手法でMarkdownを生成している。

で、今回もライブラリなしのPureJSで処理している。 今回は44行ほどだが、18行は共有化できるものなので、もう少し短くすることも可能だ。 DHTMLのお手本のような初歩的なものである。

Event.stopPropagation()Event.stopPropagationと書いて悩むはめになったけど。

PureBuilder Simply 1.4 リリース

PureBuilder Simplyの1.4をリリースした

特に大きな変更点は以下の通りだ。

  • HTML生成前に処理できるPre Pluginsに対応した
  • Pre Plugins/Post Pluginsで環境変数から文書メタデータにアクセスできるようになった

Pre PluginsはPandocにかける前のドキュメントを加工するものである。 Markdownと比べReSTは自由度が低いこと、それぞれのドキュメントフォーマットに基づいて処理しなければならないことから、新たに_docformatというメタ値が追加された。

今回のポイントはPre Pluginsであり、メタデータを渡す仕様は反映していなかっただけで、実は1.2時代からあった。

Pre Pluginsはおもしろいこと書いていないので、どちらかといえばメタデータ渡しの話をしよう。

これはPre Plugionsの一部である。 IO.popenはコマンド群の前に環境変数を置くことができる。 シェル的にいうと

みたいなことだ。

もちろん、同じような手法はRubyでも使えるけれど、ちょっとめんどくさい。IO.popenの利便性は簡便に損なわれてしまう。

かゆいところに手の続くRubyは、ちゃんとそのプロセス用に環境変数を渡す方法を用意してくれているわけだ。

予め環境変数にセットするのと何が違うのか。

まず、自身の環境変数としてセットすると、メモリーを2個分使う。

また、環境変数がそのプログラム自身の制御に影響するケースでは問題が生じる。

さらに、何度もプロセスを起動するたびにセットしてしまうと、ガベージコレクションの問題が出る可能性がある。

結局、子プロセスに対して伝播したいだけの環境変数はこのプロセスに対してのみセットするのが適切、ということになる。 シェルにおいても

ではなく、

あるいは

とすべきである。

静的ウェブページでタグ機能を提供する

Mimir Yokohamaのページでタグ機能がバージョンアップし、完全に動作するようになった。

もともとWordPressで提供していたMimir Yokohamaのウェブページだが、独自システムに移行する際には「WordPressで利用していた機能はすべて提供する」という方針のもと、新しいサイト構築システムPureBuilder Simplyを開発して構築した。

PureBuilder Simplyは静的ページを生成するプログラムであり、webサーバーには静的ファイルを配置する。 これはパフォーマンス、セキュリティ、管理、リソースいずれにおいてもメリットが大きい。

基本的にこの考え方は「異なる内容を生成するタイミングより、同一の内容を返すタイミングのほうがずっと多い」ということに基づいており、キャッシュよりも合理的である。 一方、どうしても難しい要素もある。ひとつはページ生成パターンが無限である検索機能、そしてもうひとつはヒントを日本語のみにした場合のタグ機能だ。

検索機能はGoogleに頼っているが、タグ機能は難関だった。

タグ機能を作る

方針

  • 要求タイミングでの動的生成は行わない
  • PureBuilder Simplyの枠内で解決する。 もし解決不能な場合はPureBuilder Simplyを拡張する
  • (PureBuilder Simplyでサポートされている) eRubyテンプレートは使わない。あくまでPandocテンプレートで生成する
  • ファイル名が日本語になることはやむを得ないものとする (Nginxは日本語ファイル名に対してURIエンコーディングされたパスでアクセスできる)
  • リンクを日本語で書く(エンコードをブラウザに委ねる)ことは許容しない
  • ユーザー (この場合自分だけど) にタグに関して文書にタグ付けする以上の手間をかけさせない

タグをつける

既にPureBuilder Simplyでは 「Frontmatterに文書に関する追加的情報を書く」 という仕様となっている。1

単純にこれを反映したタグを書けば良い。

ページにタグ情報をつける

Pandocテンプレートで簡単につけることができる。

英語なら割とこれで済む話なのだけど2、日本語だと当然

みたいなHTMLが生成されてしまう。

そこで、post generate機能を利用する。 post generate機能はページを生成したあと生成ページを加工できるものだ。 基本的に第一引数として生成されたファイルパスが渡される。 このほか環境変数を通じて他の情報にもアクセスできたりするのだが、これはあまり利用を想定していない。

post generateは.post_generateディレクトリ内のファイルを順次Perlに渡す形で実行される。 Perlはshebang行を解釈するので、Perlで書かなければならないわけではない。 そして、スクリプトの出力にファイルは置き換えられる。

これは例えば

のような非常に簡単なフィルタが書けるということだ。

これを使って

のように変換してあげればタグのタイトルは日本語だがURIはエンコード済み、という形ができあがる。 もちろんスラッグへのマップを書いてもいいのだが、タグを管理するのは手間なので避けた。

タグページを作る

生成されたページの情報はindexes.rbmというファイルにRuby Mershal形式で保存される。 ここには本文は含まれないが、大概メタ情報にアクセスしたい場合と本文にアクセスしたい場合は別なので、分けている。

PureBuilder Simplyにおいて、「メタ情報を文書に書き、処理した情報はデータベースに書いておく」というのは設定上の核であると行って過言ではない。 これにより、文書のメタ情報を扱うことはPureBuilder Simplyの外で行うことができるのだ。

タグページは.tagcloud.rbというスクリプトによって生成しているが、 これはMarkdownページを生成する。つまり、「タグページ自体をPureBuilder Simplyによって生成すべきページとして生成する」のである。 タグを含むページを生成・更新した場合は再度タグページを生成し直すことになり、そのためにrefreshというスクリプトもある。この場合、文書ページではないタグのindexを処理されてしまうと困るので次のような処理になっている。

では.tagcloud.rbは、というとこちらも結構単純。

見ての通り著しい力技でデータベースを集計し、最終的にはMarkdownを出力している。

従来もほとんどこうだったのだが、ページ側でエスケープしていないという理由でエスケープしていなかったので追加した。

おわりに

もう少し難しいかと思っていたのだが、どうやらPureBuilder Simplyの設計が思っていた以上に優れていたようで、タグ機能もスムーズに実装することができた。

PureBuilder Simplyは傑作といって差し支えない出来になっている。 もともと思っていたよりもずっと優れたツールになっているのだ。

PureBuilder SimplyはPureBuilderとしては実に3作目である。 Zshの機能をフル活用したPureBuilder, Windowsでも動作可能なようRubyで書かれたPureBuilder2。ページの生成にはいずれも活用できたが、サイト構築労力が高く、安価な案件で利用するにはしんどいものがあった。 また、構築できる内容も割と画一的だったため、様々な要求に応えるのは難しかった。 Zshで書かれたPureBuilderは構築時に任意のZshスクリプトを実行できる方式だったため、なんでもできるといえばできるのだが、サイト構築がプログラミング色の強いものになっていた。これはちょっとユーザーフレンドリーではない。

PureBuilder Simplyは名前の通りずっとシンプルだが、いままでよりずっと強力になった。

小さなスクリプトを書くことは、多くのプログラマにとってはあまり馴染みのないことかもしれないが3、やろうと思えば発想さえ知れば決して難しいことではないはずだ。


  1. これはReSTでもdocutilが許容しないような規格化されていないメタ情報を書くということだ。

  2. ちなみに、Chienomiではタグはすべて英語になっている

  3. Unixに浸っている人はむしろ息をするようにのように行動するだろう。PureBuilder Simplyの考え方はこれに基づいている。

LuaTex-jaでNumber too big.エラーがようやく解消

LuaTexで日本語ドキュメントクラス(ltjsarticle, ltjarticleなど)を処理すると

! Number too big.
ltj@@jfont ->luafunction ltj@@jfont@inner 

l.53 \kanjiencoding{JY3}\selectfont

と言われてしまう、という問題が続いていた。 Manjaro Forumで質問したところ、再現するけどupstreamへ、ということだったのでLuaTeX-jaのほうにご報告させていただいたところ、チケットを切っていただいた

Manjaroではつい先日Texliveが201804にアップデートされたが、この問題はLuaTeX-jaが201806として解決してくださっている。

このため、

$ git clone 'https://scm.osdn.net/gitroot/luatex-ja/luatexja.git'
$ sudo rsync -r luatex-ja/src/ /usr/share/texmf-dist/tex/luatex/luatexja/

とすればとりあえずこの問題は解決する。

だが、今度は

! Undefined control sequence.
\lltjp_um_unmag_fsize: ...@preadjust@extract@font 
                                                  \cs_gset_eq:NN \lltjp_um_f...
l.106 \begin{document}

なんて言われてしまう。 これは、

ltjsarticle + unicode-math で をしていないときに起きるエラーのようです. 別チケットにします.(#38372)

とのことで、チケットを切っていただいた

現在のところkitagawa_testブランチになっているが、恐らく近日中に取り込まれるだろう。 なかなか長い戦いだったが、ようやく論文や教材の清書も捗るというものだ。

ちなみに、この影響でgendoc-pandoc.zshのほうはMathfontに対応した。

先のシェルスクリプトを形にしました

一時キーボード無効

Temporary Disable Keyboard @GitHub

Xinputを使用してデバイスを無効/有効にするためのもの。

主な変更点は次の通り

  • 無効化を「時間制」「ダイアログ」「無限」から選択できるようにした
  • Zenityダイアログを若干調整
  • ランチャー用の.desktopファイルを追加

Zshスクリプトだが、case内で非常に珍しい;&(フォールスルーする)を使用している。

ターミナルエミュレータ選択機能

Terminal Selector @GitHub

主な変更点は次の通り

  • ランチャー用の.desktopファイルを追加
  • KDE Service用の.desktopファイルを追加
  • Nemo用の.nemo_actionファイルを追加
  • 対応する端末を大幅に増加
  • 利用できない端末を選択肢から除外するように変更
  • Zenityダイアログを調整
  • 柔軟に端末を追加できるようにファイルマネージャでの起動用の引数対応が連想配列で使用できるように変更

連想配列を使用するための方式は次のようなものだ。

わざわざ連想配列をテンプレート文字列とし、そこから配列に変換している理由は

  • 連想配列に格納できるのは文字列のみ
  • 置き換えしてからではDIRがIFSを含む可能性がある