anti scroll

ブラウザと小説の新しい関係を模索する

プロとアマの小説の特徴を数値化して比較してみたらやっぱり差があったので、それを埋めるための型付き小説記述用言語 TypeNovel を公開した件について

ラノベのタイトルみたいな記事を書く、という夢が叶いました。

github.com

開発に至った動機

以前から、アマチュアの小説はプロに比べると、描写不足な傾向があるのかもしれない、と思っていました。

特に不足がちだと感じるのは「時間」に関する描写です。

季節がわからなかったり、昼か夜か、平日か休日かみたいなことが不明瞭な作品が多い気がします。

しかし印象だけで語ってもアレなので、実際に差があるのかどうかを計測してみました。

計算式は、

時間描写の文の数 * 時間描写分布のエントロピー / 文の数

です。

「時間描写分布のエントロピー」というのは「全体を通じて、どれだけ満遍なく時間表現が書かれているか」という数字だと思ってください。

例えば時間描写が冒頭部にしかなかったりすると数値が小さくなり、全編を通じて満遍なく描写されていると、数値が大きくなります。

あと時間描写というのは、一応「季節、曜日、朝昼晩、平日・休日」みたいなことがわかる表現のことを指しています(例:初春、休日、早朝、深夜、夏休み、など)

例として、以下に夏目漱石「門」の計測結果を示します。

histgram: [30, 24, 33, 21, 29, 29, 30, 24, 24, 17]
score: 0.221913(sentence_size = 3879, total = 261, entropy = 3.298082)

histgramの部分は、全編を10分割して、それぞれの区間の時間表現の個数を記したものです。

夏目漱石の「門」は、わりと満遍なく描写されているのがわかると思います。

時間表現の個数は、形態素解析した名詞や形容詞の組み合わせから、時間表現か否かを判定するスクリプトを作って計測しました。

中身は「春」とか「夏」とか、「暗い」+「空」とか、そういう時間表現っぽい文言をいっぱい定義して、どれかに合致するかどうかを判定しただけのものです。

一応ソースです。

https://github.com/tategakibunko/time-heatmap

さて、実際に上記の計算式で計算すると、拙作サイトである縦書き文庫にてランダムに選んだアマチュアさんの平均スコアは、だいたい0.1に届かないことがわかりました。

user1: average score:0.055084
user2: average score:0.057143
user3: average score:0.020505
user4: average score:0.071971
user5: average score:0.045247
user6: average score:0.042603
user7: average score:0.086316
user8: average score:0.051926
user9: average score:0.089533

しかしプロ(文豪)の平均スコアを計算すると、だいたい0.1を超えています。

pro1: average score:0.120076, アーサー・コナン・ドイル
pro2: average score:0.210235, チェーホフ
pro3: average score:0.124371, ジェイムズ・ジョイス
pro4: average score:0.101132, ドストエフスキー
pro5: average score:0.138546, 野村胡堂
pro6: average score:0.153463, 森鴎外
pro7: average score:0.148902, クリスチャン・アンデルセン
pro8: average score:0.110636, 夏目漱石
pro9: average score:0.118976, 中島敦

重要なのは、これが「平均値」である、という点です。

個々の作品では0.1を超える作品を書いているユーザーさんもいるのですが「全作品の平均」を取ると、だいたいみんなスコアが低くなります。

上に挙げた文豪さんたちは、一般的にアマチュアさんよりも多い数の小説を出しているので、平均をとったら不利になりそうなものですが、実際はより作品数の少ないアマチュアさんよりもスコアが高くなっています。

以上の結果から、どうやらアマとプロで、少なからぬ差がありそうな雰囲気は確認できました。

というわけで、あとはこうした差を埋めるためのツールを作るだけ…ということで開発したのが、表題の型付きの小説記述用言語「TypeNovel」です。

型付き小説記述用言語「TypeNovel」とは

まず小説における「型」とはなんでしょうか?

自分は「制約(constraint)」と「注釈(annotation)」の組み合わせである、と定義しました。

小説の各シーンに「制約」を与え、作者はその制約を本文で「注釈」する。しなければ型エラー。そういう塩梅です。

見てもらったほうが早いかもしれません。

@scene({
  season:"春",
  time:"午前"
}){
  $time("朝の通勤時間")、電車の窓から、隅田川の$season("桜")が見えた。
}

上の例では、シーンが満たす「制約」として

  1. 季節(season)は「春」である。
  2. 時刻(time)は「午前」である。

と設定しています。

なので本文では、この制約を明示的に満たすように注釈(annotation)することが求められます(しないとコンパイル時にエラー)。

例えば$time("朝の通学時間")という箇所は、time制約の「午前」を「朝の通学時間」と描写することで満たそうとしています。

同様に$season("桜")も、season制約の「春」を「」と描写することで満たそうとしています。

こういう風に「各シーンに課した制約を注釈しないとコンパイルエラー」とすることで、書き手は自分が何を書かなければならないのかについて、常に自覚的になることを強制されます。

ちなみに制約は別に時間に限らず、何を設定するのも作者の自由です。例えばplace:"学校", person:"花子"とか。シーンの設計図を事前に自覚的に書く、というのが重要なところです。

先の時間描写が不足している件ですが、おそらくアマチュアの作者は、自分の頭の中では「午前か午後か」「平日か休日か」「春か夏か」などを、ちゃんと思い浮かべていることが多いのではないかと思います。

でもそれは、書かなければ第三者には伝わりません。

だから各シーンに明示的に「客観的な事実」を宣言するというお作法を強要し、かつそれを説明しないとコンパイルが通らない、というアプローチをとり、作者の客観的な自覚を促そうとしているわけです。

プログラマもよく、色々なチョンボをやらかして、コンパイラに叱られます。煩わしいけど、それによって正確なプログラムが書けるようになるわけです。

しかし小説って、基本的には誰の力も借りずに黙々と書くものじゃないですか(違ったらすみません)。

だとしたら、こういう方法でコンピューターを「機械編集者」のように利用するというのは、プログラムの質を上げるのと同じように、作品の品質を上げ得るのではないでしょうか。

作者とプラットフォームのWinWin

実はこの仕組みを導入することは、作者とプラットフォーム運営者(出版社や投稿サイト)の双方に利益があります。

作者はこの言語を使うことで、作品の質を上げることができますが、同時にプラットフォーム側も「注釈付きのHTML」を入稿してもらうことで「自然言語処理」がしやすい原稿データを取得できるのです。

例えば次のようなマークアップ

@scene({season:"春"}){
  $season("桜")が満開だ。
}

次のようなHTMLを出力します(ちなみに出力HTMLのタグや属性についてはtnconfig.jsonという設定ファイルで設定できます)。

<scene data-season="春">
  <season></season>が満開だ。
</scene>

これは自然言語処理にとっては有利です。

例えば生のテキストで「桜が満開だ」という情報から「春」という情報を機械的に取得するのは、なかなか面倒なことです。

我々がよく知る小説原稿のテキストは、あまりに形式として素朴すぎるため、自然言語処理と相性がよくないのです。

しかし出力結果が上記のようなマークアップなら、最初から作者がさまざまな情報を注釈してくれているのですから、格段に仕事が簡単になります。なにより、質の良い機械学習用のデータとしても利活用できそうではありませんか。

世の中の検索エンジンは、HTMLに意味情報を含ませることで(セマンティック・ウェブ)、検索精度を向上させてきました。

なので、小説のように「素朴な形式」で書かれる媒体も、時代とともに意味情報を含んだ形式(セマンティック・ノベル)に進化していくのではないかなあと思っています。

後述しますが、この「セマンティック・ノベル」については、個人的に仕様を模索している最中です。

インストール(2019/10/14日追加)

npmからインストールできます。

npm install -g typenovel

コンパイラの使い方

色々なオプションがありますが、基本的には次のような感じで大丈夫でしょう。

[foo@localhost] tnc mynovel.tn

上のようにすると標準出力にHTMLが出力されます。

細かいオプションについては、

[foo@localhost] tnc --help

で確認できます。

出力HTMLのタグを変えるには

詳しくはドキュメントを読んでもらうこととして、簡単に言うとtnconfig.jsonというファイルを編集することで、出力するHTMLタグを自由に編集することができます。

tnconfig.jsonというのは、もちろんTypescriptのtsconfig.jsonを真似したものです。

最初はそんなファイルはないので、コマンドラインから次のコマンドで作成します。

[foo@localhost] tnc --init

こうすると、現在のディレクトリに初期設定で書かれたtnconfig.jsonが出力されます。

このファイルのmarkupMapという欄を編集すると、色々とタグを変えることができます。

例えば@sceneというタグを<scene>じゃなくて、<div class='scene'>にしたかったら、次のようにします。

{
  "markupMap": {
    "@scene": {
      "tagName": "div",
      "className": "<name>"
    }
  }
}

上記で<name>とありますが、これはプレースホルダーといって、@sceneタグに使うと、sceneという文字列に変更されます。その他にも<arg1>, <arg2> ... とか、<nth>, <nthOfType>, <index>, <indexOfType>とか色々なプレースホルダーがあります。

詳しくはCheatsheetのページで確認してください。

開発言語:F#

開発言語なのですが、今回は実行ファイル(コンパイラ)を配布するのがメインなので、Win/Mac/LinuxにクロスコンパイルできるF#を採用しました。

F#はちょっとマイナーな言語?なのかもしれませんが、OCamlとよく似て非常に言語処理系が書きやすい言語です。

久しぶりに触ってみたら、色々な周辺ツールがよく整備されていて驚きました。

.NETの資産も活用できるので、生産性も高いし、パッケージマネージャーNugetも使いやすくていい感じです。

(2019/10/14 Update)

TypeNovelをTypeScriptで書き直しました

今後の展望

TypeNovelと並行して、SemanticNovelという仕様と、そのビューアーについて構想中です。

SemanticNovelのビューアーは、例えば

  1. シーンの切り替わるタイミングで改ページが入る。
  2. シーンの時刻に応じてビューアーのテーマが変わる(夜ならダークモードとか)。
  3. 各セリフが誰によるものなのかを明示する補助UI

などなど、作品の「文脈情報」を生かし、読者に親切な読書環境を提供することを目指して開発しています。

ただしその前に、SemanticNovel の仕様をきっちり作っておかねばなりません。ちょっと時間がかかるかもです。

で、いずれ縦書き文庫にも導入できたらなあと妄想しています。

追記(2019/07/27)

追記2(2019/11/09)

https://raw.githubusercontent.com/tategakibunko/vscode-typenovel/master/images/capture.gif

最後に

TypeNovelリポジトリオープンソースで公開されています。興味のある方はお気軽にご参加ください。

https://github.com/tategakibunko/TypeNovel

SemanticNovelについても、一応Githubに場所を用意しておきました。ぼちぼちと更新していく予定です。

https://github.com/tategakibunko/SemanticNovel

投稿画面からプレビューできるボタンを復活させました

やはりプレビューが使えないと色々と不便なので、プレビューボタンを復活させました。

投稿ボタンの下に「プレビュー」ってボタンがありますので、使ってみてください。

f:id:convertical:20190209221753p:plain
プレビューボタン

プレビューを押すと、

f:id:convertical:20190209221836p:plain
プレビュー結果

こんな感じで、簡易ビューアーが表示されます。

プレビューボタンが表示されない方は、リロードするか、それでも駄目ならタブを閉じてから開き直してみてください。

縦書き文庫をリニューアルしました

縦書き文庫をリニューアルしました。

新しくなったところ

小説投稿画面の機能がスマホでも全て使えるように

これまではスマホの投稿画面ではルビや太字ぐらいしか選べなかったのですが、すべてのボタンがPCの投稿画面と同様に使えるようになりました。

PCだとこんなふうで、

f:id:convertical:20190207121350p:plain
PCの投稿画面

スマホとかだとこういう感じです。

f:id:convertical:20190207121425p:plain
スマホの投稿画面

しおりが無制限に挟めるように

これまでは10個までの制限があったのですが、いくらでも挟めるようになりました。

以下は、しおりを入れるボタン

f:id:convertical:20190207121647p:plain
しおりを挟む

以下はしおりを開くボタン

f:id:convertical:20190207121712p:plain
しおりを開く

しおりを開くと、しおりの一覧が出るので選択します。

f:id:convertical:20190207121748p:plain
しおりの一覧

ちなみに、しおりの数に制限はありませんが、一つの作品に大して10個以上の栞を挟んだ場合、直近の10件までが表示されます。

マテリアルデザインの採用

スマホ等のタッチで操作する端末に親和性が高い(とされている)マテリアルデザインを採用しました。

色々な操作がアニメーションするので、操作がより直感的になったのではないでしょうか。

シリーズ作品の並び替えがドラッグ・アンド・ドロップに対応

シリーズ作品の並び替えも、より直感的にドラッグ・アンド・ドロップで可能になりました。

f:id:convertical:20190207122216p:plain
シリーズの並び替え

ダッシュボードに読んだ、読まれたの情報を表示

ダッシュボードページを新設し、作品を読んだ、読まれたの履歴がパッと確認できるようになりました。

f:id:convertical:20190207122435p:plain
ダッシュボード

「読まれた」のタブには、自作がページ送りされてポイントが入った履歴が出ます。

「読んだ」のタブには、自分が他者の作品を読んで得たカルマの履歴が出ます。

廃止された機能について

埋め込み作品の廃止

利用者があまりいないのと、サーバーへの負担を考慮して廃止となりました。

一部の古いコードは動くかもしれませんが、動作は保証されません。

キャラクターとファンのタブ(プロフィールページ)を廃止

スマホの横幅に足りないというのが主な理由なのですが「ファン」についてはPC限定で復活するかもしれません。

表紙画像の投稿機能を廃止

廃止したのは、表紙の有無が作品のクリック率にあんまり寄与しない(ように見える)ほか、著作権の確認なども大変だったからです。

賛否両論あるかもしれませんが、表紙画像をなくすことで、サイトの読み込みも速くなるので、自分としては良かったかなあと思っています。

開発の振り返り

去年の年末にPCがぶっ壊れてしまったのですが、奇跡的にデータが残ったまま修理が完了したので、そのまま最後まで開発することができました。

自分にとっては今回が人生初Angular(とNgrx)だったのですが、すごく開発しやすかったです。

Angular+Ngrxでは、状態をObservableで閉じることができるので、論理的に矛盾した状態が作られにくい、というところが素晴らしいと思いました。

Angular + Ngrx所感

先月から試験的にAngular+Ngrxを使って縦書き文庫を作り直してみる、ということをしていたんですけど、パソコンが壊れバックアップも取ってなかったので、全て消え去りました(この記事はChromebookで書いてます)。

真面目にやると一年ぐらいかかる作業なので、一ヶ月程度の作業で済んだのは不幸中の幸い?なんでしょうかね。。。

Angular+Ngrxの感想としては、コツを掴むまでかなり時間がかかるということと、fluxなんでaction/reducer/state/effectの開発ループを回すのが面倒くさいなあ、ということです。

正直なところ「このまま続けるべきか?」と思ってたところなんで、もしかしたら中止できてよかったのかも。。。

学習コスト

TypescriptとRxに関しては多少ですが経験があったのでなんとかなりそうだったのですが、それ以外に覚えないといけないことが沢山あって大変でした。

よくAngular+Ngrxは大規模なチーム開発に最適などと言われますが、学習コストを嫌ってVueという選択をする人が多いのも、なんとなくわかるような気がします。

Ngrxについて

Ngrxを採用すると、とにかくRxでEffectsを書きまくることになるのですが、ここで割とわかりにくい地雷を踏みまくってしまいました。

特に危険なのはストリームを止めちゃう位置にcatchErrorを書いちゃったり、逆にストリームを止めるべきところでdispatch:falseを指定しないで無限ループを仕込んじゃったりとかです。

地雷1 catchErrorの場所

まずEffects中のcatchErrorですが、actions$直下のpipeではなく、map中のpipeで宣言しないといけません。

@Effects()
getFoo$ = this.actions$.pipe(
  ofType("foo"),
  switchMap(action => 
    return fetchFoo(action.payload.id).pipe(
      map(result => new GetFooDone(result.body)),
      // エラーは内側のpipeで補足する
      catchError(error => of(new GetFooFail({error})));
    );
  ),
  // ここに書いてはいけない
  // catchError(error => new GetFooFail({error}))
);

上の例は、fooに対応するactionを直下のactions$からフィルタリングし、そのデータを元にfetchFooでサーバーから何かしらのデータを取得しようとするコードです。

この処理の失敗を、action$直下のcatchErrorで補足すると、fetchFooでエラーが起きたときだけじゃなく、大本のactions$で起きたエラーも補足してしまいます。

上の例だと、GetFooFailというアクションを出したいのは、fetchFooが失敗したときだけですよね。

気をつけましょう。

地雷2 別のアクションに変化しない副作用

Effectsが新しいActionを返さないのにdispatch:falseをデコレーターに設定しないと、永遠にその処理が繰り返されます。

@Effect({dispatch: false})
logFoo$ = this.actions$.pipe(
  ofType("foo"),
  tap(_ => console.log("foo"))
);

上の処理で{dispatch: false}を忘れると、無限ループです。

自分はCPUがうるさくてパソコンが熱くなったので気づきました。自動ビルドで開発してたのですが、開いてる先のタブがいつのまにかCPU使用率100%になっていたりとか。

これがパソコンの熱暴走を起こし、寿命を縮めたんじゃないのか、と今では疑っています。

地雷3 Angular Materialとの相性

ngrx/storeの状態をangular materialのコンポーネントのディレクティブにダイレクトに反映させてしまうと、アニメーションが効かなかったり(例えばチェックボックスとか)、一部のUI要素が正しく計算できなくて表示できなくなったり(例えばタブコンポーネントの下線)みたいなことがあります。

なのでマテリアルコンポーネントを使用するときは、コンテナーというUIクッションを用意して、マテリアル要素はその下に置くようにします。

で、マテリアルなコンポーネントを含む下層のコンポーネントには「状態のコピー」を渡し、ローカルでその状態を操作させるようにします。そうしないと、アニメーションが正しく機能しないからです。その上で、子供の最新状態は@Outputを使ってコンテナーに吸い上げます。

つまり一元管理された状態を使用して制御できるのはコンテナーUIまでで、そこから下は状態のコピーが作成されることになります。全てが理想通り、とはいかないですね。

余談1

ちなみに壊れたMacbookの修理はざっくり7万ぐらいとか言われたんですけど、まあ買い直すよりかは、と思って依頼しちゃいました。

でも年末で忙しいから14営業日かかるとのこと。。。今からだと来年の10日前後?

余談2

そういえば最近、中国の開発者が「Immutableなんてほとんどのケースで必要ない。本当の未来はshadow-domを使い、素直にmutableな構造で、かつVueっぽいものだ」みたいなコンセプトでなんかフレームワーク作ってて、すごいスターをもらってた記憶があるのですが、名前を忘れました。

AngularのExpressionChangedAfterItHasBeenCheckedErrorに関する覚え書き

ネットでExpressionChangedAfterItHasBeenCheckedErrorを検索すると、よく「子が親の状態を変更するとビューに一貫性がなくなることが警告される」という説明を目にしますが、少し混乱を招きかねない説明なんじゃないかなあと思いました。

正確には「Angularが変更検知をする際に行う探索順において、後からチェックされるビューから、先にチェックされるビューの状態を変える処理を書くと警告される」と説明すべきなのではないでしょうか。

なぜなら、HTMLには親子関係だけではなく、隣接(兄弟)関係というものがあるからです。親が子より先に探索されるのは当たり前ですが、隣接関係については自明ではありません。

例えば次のようなツリーがあったとします。

A, B
|
C

AとBは隣接(sibling)関係で、Cの親はAです。このときCの親であるAがCより先に探索されることは自明ですが、Bはどうでしょうか。Cより先でしょうか、後でしょうか。

もしAngularの変更検知が「幅優先探索」の順で行われるなら、Bが先です。しかし実際のAngularの変更検知順は「深さ優先探索」なので、検知される順に番号をふると、次のようになります。

A(1), B(3)
|
C(2)

つまりBは、Cよりも「後に」変更検知されるわけです。

だから探索順に置いて最後になるBにおいて、Cが依存するような状態を変更すると(先に確定されたCの状態を後からBが変えることになるので)ExpressionChangedAfterItHasBeenCheckedErrorというやつが出ます(ただし警告してくれるのは開発ビルドのときだけ)。

よくウェブで見る「親と子」という説明の仕方だと、幅優先か深さ優先かで受け取る人の解釈が曖昧になるので「探索順」を使った説明をしたほうが親切なのではないでしょうか。

Typescriptのアレコレ覚え書き

書いておかないと忘れちゃいそうなので、残しておきます(たまに追記するかも)。

戻り値がbooleanであると同時に、引数が特定の型であることをコンパイラに教えることができる

これが便利なのは、こういう感じのコードにおいてです。

class Foo {
  say(){
    console.log("foo");
  }
}

let isFoo = (val: any): boolean => {
  return val instanceof Foo;
}

このisFooを使って、ある値の型がFooである場合に(Fooのメソッドである)sayを呼ぶ、といった処理を書きたくて

let func = (val: any) => {
  if(isFoo(val)){
    val.say(); // val は any なのでエラー
  }
}

などとやると、コンパイル・エラーが出ます。上のコードでは(当たり前ですが)valの型がanyとみなされるからです。

しかしこれでは、せっかくFooであることを確認するisFoo関数の意味がなくなってしまいます。

しかしこの問題は、isFooを、次のように書き換えることで解決します。

let isFooEx(val: any): val is Foo {
  return val instanceof Foo;
}

戻り値の型の部分に記述されたval is Foo の働きによって、引数のvalの型は呼び出し元にFooだよ、と認識され、次のコードが通るようになります。

let func(val: any){
  if(isFooEx(val)){
    val.say(); //  val は Foo なのでOK!
  }
}

このval is Fooの部分のことを、Type Guard と呼ぶのだそうです。

詳しくは、Type Guards and Differentiating Types を参照して下さい。

型引数にもデフォルト値を設定できる

TypeScript 2.3からですが、次のような感じで型パラメータにデフォルト値を設定できるようです。

class Component<Props = any, State = any> {
  props: Props;
  state: State;
}

type StateActionPair<T, V extends Action = Action> = {
  state: T | undefined;
  action?: V;
};

二番目の例にあるV extends Action = Action の部分は、Actionの部分型であるVのデフォルト型はActionである、という意味です。

これまでこういったことを知らなくて、色々と汚いコードを書いてました…反省。

Nullチェックを無視させることができる

nullが入り得る型なんだけど、そのチェックがいらないことが明らかな場合、コンパイラnullチェックを無視させることができます。

let foo = (entity?: Entity): string {
  return entity!.name;
}

entity!という部分がそれです。

上のプログラムにおいてentityという引数は、Entity型もしくはnullもしくはundefinedになる恐れのある引数だ、と言ってるので、普通にentity.nameと書くと、コンパイルエラーになります(ただし--strictNullChecksが有効時)。

しかしこういうnullなどになり得る変数のあとに!をつけると「ここは特別にnullにならないからチェックしなくて良いよ!」という意味になります。

これも使えると時々は便利です。

参考:Non null assertion operator

undefinedならプロパティアクセスさせず全体をundefinedにする演算子

例えばこんなオブジェクトがあるとして

const obj: any = {
  foo: {
    bar: 'bar'
  }
};

次のようにアクセスするとエラーになります。

const value = obj.bar.bar //  Cannot read property 'bar' of undefined

こういう木構造の途中がundefinedかもしれないものにアクセスするときに、いちいち

const value = obj.bar? obj.bar.bar : undefined;

みたいに書くのは面倒ですよね。しかし?演算子を使うと、これを

const value = obj.bar?.bar;

というように、短く書くことができます。

仮にobj.barundefinedでも、obj.bar?で評価が止まり、全体がundefinedになるわけですね。

opam2のインストールについて

2018年9月にようやく正式リリースされた opam2

公式が提供するスクリプトでインストールできれば問題ないのですが、それだとうまくいかない環境もあり、色々とインストールが面倒だったのでメモしておきます。

ちなみに以下の内容は次のスクリプトで成功する人には何の価値もない内容ですのでご注意ください。

sh <(curl -sL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)

で、どうやってインストールしたかというと、自分の場合はソースからインストールしました…

ocamlbrewなる便利そうなのもあるのですが、ここ一年ぐらい更新されておらず、opam2が正式リリースされたのが先月(2018年9月)というのも考慮し、今回は見送りました。

なおopam2は4.02.3以降じゃないとビルドできませんので、先に

opam switch

とやって表示される中で、4.02.3以降のコンパイラに変更する必要があります(もちろんこれは古いままのopamで実行しても問題なし)。

自分は素直に現時点での最新バージョンを入れました。

opam switch 4.07.0

コンパイラのバージョンに問題がないなら、opam2.0.0のソースディレクトリに入ってビルドします。

./configure
make lib-ext
make
sudo make install

makeするまえにmake lib-extが入ることに注意してください。

ちなみに最後のsudo make installなのですが、ホームディレクトリの.opam/<ocamlのバージョン>/bin/以下にあるツール(jbuilderocamlfind)を使うので、普通に実行しようとすると、おそらくrootがそれらを見つけられずにコケます。

よってsudoerを編集します(他にもっといい方法が絶対にありそうなのですが、思いつかなかった…)。

visudoを起動して

Defaults env_keep += "PATH"

を追加して、

#Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin

コメントアウト。ついでに.bashprofileなどに

PATH:=$PATH:~/.opam/4.07.0/bin
export $PATH

みたいなのも追記しておきましょう(終わったらsource .bash_profileも忘れずに)。

これでローカルのjbuilderやらocamlfindやらのパス解決もできて、sudo make installが無事に通ります。

さて、インストールが終わると、新しいopamを利用するためのセットアップ処理が走るのですが、途中でbubblewrapという、サンドボックス環境を使うための仕組みを設定するかどうかを聞かれます。

セキュリティーを考えたら使ったほうが良さそうなのですが、後で調べてみたら自分の環境ではインストールできないツールだったので、インストール後に.opam/configから以下のbubblewrapに関する設定を削除しました。

# 以下の3つの設定を消す
wrap-build-commands:
  ["%{hooks}%/sandbox.sh" "build"] {os = "linux" | os = "macos"}
wrap-install-commands:
  ["%{hooks}%/sandbox.sh" "install"] {os = "linux" | os = "macos"}
wrap-remove-commands:
  ["%{hooks}%/sandbox.sh" "remove"] {os = "linux" | os = "macos"}

ちなみに理由はよくわかりませんが、先の質問に「No」と答えても、上の3つの設定は.opam/configに追加されてしまうので注意してください。

セットアップ用の質問に答え終わると、すごく長い「何かしらの処理」が始まります。

途中経過とかが全く出てこないので「え、フリーズ?」と心配してしまうかもしれませんが、気長に待ってたらちゃんと終わるので、安心して下さい。

さて、初期化の処理が終わると、これまでopam1.2系列を使っていたことで止まっていたopam upgradeが、また進むようになります。

拙作のjinja2互換テンプレートエンジンであるjingooも、opam1.2系列では1.2.18までしかインストールできませんが、opam2にすると1.2.19以降もインストールできるようになります。

公式の説明を見る限り、もうopam1.2系列はサポートしないらしいですし、既に新しいパッケージを公開する際は、opam2.0系列を強制されてしまいます。

これで先月はopam-repositoryへの新パッケージの申請が軒並みコケていて、大混乱でしたね。自分もその一人でした。

バージョン2系列はバージョン1系列のopamファイルと互換性がなく、微妙にフィールド名が変わっていたりするので注意が必要です。

自分の場合は、とりあえずopam-repositoryにopam1のままのpull requestを投げてみて、弾かれたらエラーログを見ながら直す、みたいにして、体で覚えました。

nehan version6 を公開しました

nehan version6 を公開しました。

github.com

version6はnehan.jsではなく、nehanという名前で開発することになりました。

それに伴い、リポジトリのURLなども変わっているので、ご注意ください。

変わったところ

  • Typescriptで書きました(かなり開発しやすくなりました)。
  • npm経由で使えるようになりました。
  • 組版がより正確になりました。

インストール

npm install nehan

使い方

Typescriptなら、

import * as Nehan from "nehan";

Javascriptなら

let Nehan = require("nehan");

注意点

過去バージョンのnehanもそうでしたが、古いnehan.jsとの互換性はまったくありませんので、ご注意ください!

あと古いブラウザはざっくりと切り捨ててしまったので、動かないかもしれません。

ちなみに縦書き文庫については、古いブラウザも当面サポートしなければならないので、旧バージョンのnehan.jsのまま動かしていますが、最新のNehan Readerは、このversion6を使って動かしています。もし良かったら、試してみてください。

chrome.google.com