scrollブラウザと小説の新しい関係を模索する2019-07-11TypeNovelにおいて、注釈しなくても良い制約を明示的に宣言できるようになりましたTypeNovelどういうものか次のように、制約の値を"?"で始まる値にすると、その制約は本文中で注釈されなくてもエラーになりません。思われるかもしれませんが、敢えて宣言したい場合があります。「時刻が"?"のとき」は「不明瞭な時刻のようなもの」を連想させるために、ビューアーのテーマをグレースケールにさせたい、とか。「時刻が不明瞭である」という情報を、HTMLを扱うアプリケーション側が欲しがる場合です。上の例をコンパイルすると、@sceneブロックは次のようなHTMLにコンパイルされます。本文中でtimeの注釈をしなくてもエラーにならない!</scene>このように、HTMLにdata-time="?"という形で「時刻が不明瞭ですよ」という情報が盛り込まれるわけです。いうわけで今回、新しい仕様を付け足すことにしました。これにより、制約値が不明瞭であることをHTMLに反映しつつ、TypeNovel側の本文では注釈しなくてもエラーが出ないようになりました。v0.9.1から有効になりますので、興味のある方は最新バージョン(すでにダウンロードできるようになっています)をご利用下さい。最初にWindows用とLinux用のexeのzipができましたので、ご利用下さい。Windows用のzipLinux用のzipMac用のインストーラー(pkgファイル)まずは適当な文章を書く適当にこんな感じの文章を書いたとします。}}リリースモードでコンパイルこれをリリースモードでコンパイルします(リリースモードは、途中の余計な空白を取り除くモードです。出力結果がコンパクトになります)。sample.htmlコンパイルのexeがMacでは小文字のtncですが、Windowsでは大文字のTnc.exeだと思います。各自ここは書き換えて使って下さい。できあがったsample.htmlを見ると、こんな感じになっています。data-time='night'><time>日も暮れたし</time>もうそろそろ帰らない?</scene></scene>冒頭に「season制約(summer)を注釈してないよ!」というエラーが出ていますが、ここでは気にしないことにします。縦書き文庫のビューアーにドラッグ・アンド・ドロップとりあえずhtmlファイルができたので、縦書き文庫に行って、適当な作品を開きます。開いたら、作品の本文部分にsample.htmlをドラッグ・アンド・ドロップしてみましょう。するとダイアログが開くので、一般には次のように設定して「読み込む」を押します。確認ダイアログすると次のように内容が表示されます。表示結果(おかしい)しかしちょっと変だということに気付いたでしょうか。冒頭のエラーのことではありません。別々の@sceneで分けたブロックが、すべてつながって一行の中にまとめられてしまっているのが問題です。こんなことになるかというと「本来HTMLに<scene>なんていうタグはないから」ということになります。一般に登録されていないタグは、インライン・タグと認識されるのがお約束です。<scene>は<b>とか<span>とかと同じようなタグと認識されたので、全ての文章が一行につながってしまったわけです。設定ファイルを作成するこれをどうにかするために、まずは次のコマンドを打って、tnconfig.jsonを作成します。--init上のコマンドをうつと、作業ディレクトリにtnconfig.jsonというファイルができるはずです。このtnconfig.jsonをエディタで開いてみると、途中に@sceneタグを<scene>ではなく<div>タグにマッピングする処理が書かれています。次がそれに該当する箇所です。class='scene'>に変える、と書いてあります。class="scene">として出力されそうです。いっかい先程と同じようにリリースモードでビルドし、出来上がったhtmlを見てみると、今度は次のようになっています。data-time='night'><time>日も暮れたし</time>もうそろそろ帰らない?</div></div>今度は<scene>タグではなく、<div>タグで出力されていまるのがわかりますでしょうか。<div>というのはもちろん通常のHTMLに用意されているタグで、ブロックタグを表すタグですから、今度は別々のシーンが別々の段落になっているはずです。だいたい望み通りの出力です。いうわけで、こうやって縦書き文庫を利用することで、一応は出力結果を本のようにして読むことが可能になっています。ちなみに「縦書きは嫌だ!」とか「文字が小さい!」とか不満がある方は「表示設定」というボタンから変更できます。専用のビューアーは別に作るつもりですが、しばらくはこれでご容赦いただきたく。.NET調べてみたのですが、日本語の文献がほとんどなかったので、ここに記しておきます。間違いなどありましたら、指摘していただけますと幸いです。予備知識PaketとFAKEの説明の前に、dotnetの予備知識などを少々。管理単位「ソリューション」と「プロジェクト」プロジェクトは個々に別々のプログラムを指します。アプリケーション本体とかテストプログラムとか。ソリューションは、プロジェクトをまとめた単位です。monoアプリケーションとその実行についてdotnetでは色々な*.exeを必要とすることが多いのですけど、*.exeの形式になってるのは大抵monoのアプリケーションです。いうプログラムは(少なくともMacでは)単体での実行ができないので(Windowsではどうか知らない)、次のようにmonoコマンドを経由する必要があります。some_app.exePaketのインストールちょっと紛らわしいですが、Packetではなく、Paketです(間の「c」がいらない)。5.215.0とします。通常はソースをcloneした人が個別にそんなことをしなくてもいいように、paket.bootstrapper.exeというものを提供する方式が一般的なようです。その場合どうやって運用するかというと、ソリューションのルートディレクトリに.paketというディレクトリを作る。initとすると、Paketを実行するために必要なものが用意される。Modeと呼ばれる使い方です。Modeのpaket.exe(実体はpaket.bootstrapper.exeで70kbyteぐらいしかない)は、初めて起動したときに本体のpaket.exe(8Mbyteもある)をネット越しにとってきて、dotnetの一時ディレクトリに展開します。それ以降はpaket.exe(実際はpaket.bootstrapper.exe)に対するコマンドは、その本体に対して送られるようになる、ということみたいです。Modeということなんだと思います。本体のpaket.exeは頻繁に更新されるし、サイズもでかいのでリポジトリに入れるのはしんどい。めったに更新されないpaket.bootstrapper.exeをpaket.exeとしてリポジトリに入れておき、それを経由してpaket.exe(本体)を実行する仕組みにすれば、paket.exe(本体)の更新に強くなる。いった理由だと思います。Modeのpaket.exe(つまり本当はpaket.bootstrapper.exe)のことを指すのか、本体のpaket.exeなのかを、文脈で判断する必要があって、ちょっとややこしいです。色々な管理ファイルがありますが、先に説明したソリューションとプロジェクトという単位を知らないと混乱します。paket.dependenciespaket.dependenciesはソリューション単位の依存関係を記述するファイルです。ソリューションはプロジェクトの集合でしたから、つまりは全プロジェクトを横断して必要なパッケージを記述するファイルになります。fileを参照して下さい。ちなみに自分の場合は、objectxさんに全ておまかせしてしまいました(笑)。paket.referencespaket.referencesはプロジェクト単位の依存性を記述するファイルです。当該プロジェクトで必要とする依存性だけを記述します。先のpaket.dependenciesとは違い、単にパッケージ名を羅列するだけで良いみたいです。TypeNovelの場合、コンパイラ本体のプログラム(プロジェクト名はTnc)のpaket.referencesはこんな感じです。FSharp.CoreArguで、コンパイラが参照するTypeNovel用のライブラリ(プロジェクト名はLib)だとこうなっています。restoreなど)を利用して依存性を更新すると、自動的に作られるファイルです。paket.dependenciesに記述したすべてのパッケージが間接的に必要とするものまで含めて、すべての依存性がこのファイルに記述されます。.paket/Paket.Restore.targetspaket.lockと同様に、パッケージの更新やインストールによって、自動で更新されるファイルです。.paket/Paket.Restore.targetsは、*.fsproj(各プロジェクトのルートディレクトリにあるプロジェクト構成ファイル)にて、次のように取り込まれます。/>というタグがそれです。このタグによって、使用しているパッケージへの参照が通ります。[コマンド名]として下さい。以降は記述を簡潔にするため、グローバルにインストールされている体裁で説明します。コマンドがあるのですが、ようするにpaket.lockというファイルの扱いがそれぞれ異なるからコマンド名が分かれている、と考えればわかりやすいです。outdated新しいバージョンが用意されているパッケージの一覧を表示してくれます。一覧するだけなので、paket.lockファイルには影響がありません。installpaket.lockの内容に従い依存関係を調べ、必要なパッケージをインストールしてくれます。paket.dependenciesに変更があった場合は、先にpaket.lockの更新処理が入ります。update現在の依存関係を更新して、必要ならば新しいパッケージをインストールします。[パッケージID]とすると、指定したパッケージだけが更新されます。このコマンドを走らせると、まず最初にpaket.lockファイルが削除されて、一から作り直されます。この新しいpaket.lockを元に(必要ならば)新しいパッケージがインストールされます。installとは違い、paket.lockを作り直してからインストールすることに注意して下さい。restoreこのコマンドを走らせると、paket.lockファイルが更新されます。更新されるだけで、インストールまではされません。次のように--references-fileオプションで各プロジェクトのpaket.referencesを指定することもできます。installFAKEについてFAKEはF#のMakeです。ドメイン固有言語で記述するということになっていますが、実体は普通のF#コードです。概念についても通常のMakeと同じで、それぞれの生成物(Target)が何に依存し、何を実行して作られるのか、みたいなことを記述します。ちなみにfake-cliがインストールされてなかったら、勝手にダウンロードして実行、みたいなことまでしてくれるみたいです。/path/to/toolとやります。installのグローバルインストール先は、初期設定では$HOME/.dotnet/tools/です。テンプレート作成Fakefileをその都度つくるのはしんどいので、テンプレートから雛形を作ることができます。テンプレート一覧みたいなのを次のコマンドで取得します。"fake-template::*"で、テンプレートの作成は次のようにします。fakeすると、カレントディレクトリに.fakeディレクトリ、build.fsx、fake.cmd、fake.shが作成されます。fake.cmdはWindows用のビルドスクリプトで、fake.shはMac/Linux用です。Make処理を書く先はbuild.fsxで、中身は普通のF#コードです。こんな感じになっています。"All"Targetの作り方Target.create関数を使います。buildというコマンドラインの処理をしてくれます。"Build"https://gist.github.com/KarandikarMihir/1b6903ef77655ed65bd2f6c6c1cd618f#file-build-fsx上の==>とかは、Fake.Core.TargetOperatorsからインポートされたものです。上のコードの意味するところはBuildしたかったらRestoreしろ、RestoreしたかったらCleanしろ、ということです。実行される順番を記述したもの、と考えてもいいかもしれませんね。BuildはRestoreに依存し、RestoreはCleanに依存している、ということだから、makeで書くとこんな感じでしょうか。clean"FakeのOperatorsFakeには記述を読みやすくするための二項演算子がたくさん定義されています。見ておいたほうがよさそうなのは、TargetOperatorsFileSystemOperatorsGlobbingOperatorsです。GlobbingOperatorは検索するファイルの候補のリストを検索して作成するものなんですが、こんなイメージ。[x]!!は引数をIncludeのただ一つの候補として初期化。++はそのIncludeの候補に追加。"Build"というのは、Buildの前にCleanを実行するけど、Cleanが実行できなくてもBuildを実行しちゃってもいいよ、という緩やかな依存関係を意味します。"All"は、Allの処理をするまえにCleanは絶対に実行しなくちゃ駄目だよ、という厳密な依存関係です。最後になんていうか、知らない間にすごい洗練されていたんだなあと実感しました。typescriptラノベのタイトルみたいな記事を書く、という夢が叶いました。以前から、アマチュアの小説はプロに比べると、描写不足な傾向があるのかもしれない、と思っていました。不足がちだと感じるのは「時間」に関する描写です。季節がわからなかったり、昼か夜か、平日か休日かみたいなことが不明瞭な作品が多い気がします。印象だけで語ってもアレなので、実際に差があるのかどうかを計測してみました。「時間描写分布のエントロピー」というのは「全体を通じて、どれだけ満遍なく時間表現が書かれているか」という数字だと思ってください。時間描写が冒頭部にしかなかったりすると数値が小さくなり、全編を通じて満遍なく描写されていると、数値が大きくなります。み、など)例として、以下に夏目漱石「門」の計測結果を示します。3.298082)histgramの部分は、全編を10分割して、それぞれの区間の時間表現の個数を記したものです。夏目漱石の「門」は、わりと満遍なく描写されているのがわかると思います。時間表現の個数は、形態素解析した名詞や形容詞の組み合わせから、時間表現か否かを判定するスクリプトを作って計測しました。中身は「春」とか「夏」とか、「暗い」+「空」とか、そういう時間表現っぽい文言をいっぱい定義して、どれかに合致するかどうかを判定しただけのものです。https://github.com/tategakibunko/time-heatmapさて、実際に上記の計算式で計算すると、拙作サイトである縦書き文庫にてランダムに選んだアマチュアさんの平均スコアは、だいたい0.1に届かないことがわかりました。score:0.089533しかしプロ(文豪)の平均スコアを計算すると、だいたい0.1を超えています。重要なのは、これが「平均値」である、という点です。個々の作品では0.1を超える作品を書いているユーザーさんもいるのですが「全作品の平均」を取ると、だいたいみんなスコアが低くなります。上に挙げた文豪さんたちは、一般的にアマチュアさんよりも多い数の小説を出しているので、平均をとったら不利になりそうなものですが、実際はより作品数の少ないアマチュアさんよりもスコアが高くなっています。以上の結果から、どうやらアマとプロで、少なからぬ差がありそうな雰囲気は確認できました。いうわけで、あとはこうした差を埋めるためのツールを作るだけ...ということで開発したのが、表題の型付きの小説記述用言語「TypeNovel」です。型付き小説記述用言語「TypeNovel」とはまず小説における「型」とはなんでしょうか?自分は「制約(constraint)」と「注釈(annotation)」の組み合わせである、と定義しました。小説の各シーンに「制約」を与え、作者はその制約を本文で「注釈」する。