anti scroll

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

jingoo 1.4.0 をリリースしました

jingoo 1.4.0をリリースしました。

github.com

新しくなった点

  • Jg_templateモジュールを部分的に改良したJg_template2というモジュールが追加されました。

サンプル

これまでJg_templateモジュールは、次のようにmodels連想リストを与えていました。

open Jingoo
open Jg_types

let result = Jg_template.from_string "{{ msg }}" ~models:[
 ("msg", Tstr "hello, world!");
]

Jg_template2モジュールからは、models名前を与えたら値を返す関数を渡します。

open Jingoo
open Jg_types

let alist = [("msg", Tstr "hello, world!")]
let result = Jg_template2.from_string "{{ msg }}" ~models:(fun key ->
  try List.assoc key alist with Not_found -> Tnull
)

これの何が良いのかというと、string -> tvalueという型の関数にさえなっていれば、データソースは何でも良くなることです。

例えばデータソースを(連想配列より高速な)Hashtblにすることもできます。

open Jingoo
open Jg_types

let htbl = Hashtbl.create 1 in
let () = Hashtbl.add htbl "msg" (Tstr "hello, world!") in
let result = Jg_template2.from_string "{{ msg }}" ~models:(fun key ->
  try Hashtbl.find htbl key with Not_found -> Tnull
)

データソースなしで、単なる関数にしてしまうと、さらに高速です。

open Jingoo
open Jg_types

let result = Jg_template2.from_string "{{ msg }}" ~models:(function
 | "msg" -> Tstr "hello, world!"
 | _ -> Tnull
)

関数にすることで、乱数値みたいなものを扱うこともできます。

open Jingoo
open Jg_types

let result = Jg_template2.from_string "{{ msg }}, {{ rand }}" ~models:(function
 | "msg" -> Tstr "hello, world!"
 | "rand" -> Tint (Random.int 100) (* 0 ~ 100 の値をランダムに返す *)
 | _ -> Tnull
)

将来的にはこっちをメインのAPIとして使用したいのですが、旧版のAPIで既に広く使用されてしまっているので、今回はモジュールを別名で分けることで対応しました。

参考

In Jingoo.Jg_template, provide a function for the getting variables · Issue #112 · tategakibunko/jingoo · GitHub

jingoo v1.3.1をリリース

久しぶりになりますが、jingooのv1.3.1をリリースしました。

Release v1.3.1 · tategakibunko/jingoo · GitHub

変更点

  • 演算子として、新たに+=, -=,*=, /=,%= がサポートされました。
  • 匿名関数がサポートされました。
  • 条件分岐の構文にて、elseif だけではなく、else ifを使うこともできるようになりました。
  • macro ~ endmacro構文にて、endmacroの後に、macro名を付け足すことができるようになりました。

匿名関数はこんなふうに使います。

{# 2と表示される #}
{{ ((i) => i + 1)(1) }}

最後のmacro~endmacroについては、こんな風に使います。

{% macro li (content) %}<li>{{content}}</li>{% endmacro %}

{# v1.3.1からは、こういうふうにも書ける #}
{% macro li (content) %}<li>{{content}}</li>{% endmacro li %}

マクロの中身が長くなったときに「あれ、これってなんのマクロの宣言だっけ?」とかならないように、endmacroの部分で、メモ代わりみたいな感覚でマクロ名を付け足すこともできるようになった、ということです。

ただし次のように、宣言したマクロ名と違う名前を付け足すと、構文エラーになります。

{# エラー(マクロ名はliであって、fooではない) #}
{% macro li (content) %}<li>{{content}}</li>{% endmacro foo %}

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を投げてみて、弾かれたらエラーログを見ながら直す、みたいにして、体で覚えました。

jingoo v1.2.13 release

jingoo version 1.2.13 をリリースしました。

https://github.com/tategakibunko/jingoo/releases/tag/v1.2.13

前々から「UTF8モジュールのためにBatteriesは大きすぎる」という声が多かったので、それに応えてuutfというライブラリを採用したPull Requestを取り込みました。

https://github.com/dbuenzli/uutf

uutfはopamからじゃないとインストール出来ないのですが、事情があってopamを使えない人は、Makefileを付与したものを自分がforkしてるので、よかったら使って下さい。

https://github.com/tategakibunko/cmdliner

https://github.com/tategakibunko/uutf

ちなみにcmdlinerは、uutfの依存パッケージです。先にcmdlinerをインストールしてから、uutfをインストールして下さい。

どちらのパッケージも、srcディレクトリに入って、make; make installでいけますが、ポータビリティは考慮してないので、問題があるようならMakefileを編集して下さい。

Prepared Statementを少し使いやすくするeps(Extended Prepared Statement)

通常のPrepared Statementを少し使いやすくする処理系 eps を作りました。

epsはExtended Prepared Statementの略です。

github.com

簡単に言うと、こんな感じでPrepared Statementを記述したくて作ったものです。

prepare foo(age:int, name:text="no name!") as
select * from people where name={name} and age={age};

ようするに

  • ラベル付き引数を使いたい
  • デフォルト引数を使いたい
  • ついでに型を考慮した呼び出し側のコードが出力したい

わけでした。

使い方

上のSQLtest.sqlというファイルで保存したとして、

eps.exe -input test.sql -format sqlとすると

prepare foo(int, text) as
select * from people where name = $2 and age = $1;

が出力されます。また

eps.exe -input test.sql -format ocamlとすると

let prep_foo = 
  "prepare foo(int, text) as select * from people where age = $1 and name = $2;"

let exec_foo ~age ?(name="no name!") () = 
  Printf.sprintf "execute foo(%d, '%s');" age name

のように、デフォルト引数、ラベル付き引数、型制限が付いたコードが出力されます。

メリット?

  1. 通常のプレースホルダー($1,$2みたいなの)じゃなくて、ラベル名を参照したSQLが書ける => うっかり割り当てを間違えたSQLを作る危険性が減る。
  2. 型付きかつラベル引数のOCaml関数で読み出せる => 呼び出し方を間違える危険性が減る。
  3. ついでにデフォルト引数を宣言できる。

現在ターゲットに指定できる言語はOCamlSQLだけですが、そのうちサポート言語を付け足すかも?

GADTというものを知った

最近OCamlをようやくversion4系列にアップデートしたのですが。

その際にGADTとかいう言葉が気になったので調べていたら、ちょうどわかりやすいエントリーが。

Detecting use-cases for GADTs in OCaml

上のエントリによると、どうやらGADTというのは「代数型に型の強制を付けるためのもの」らしいです。

どいういうことかというと、例えばとあるexprという代数型を

type expr = 
  | Tint of int
  | Tbool of bool

と宣言した時、exprは中身がintの時もあれば、boolの時もある型ですよ、と宣言しているわけですが…

つまりTint 10Tbool falseも、同じexpr型に属することになりますよ、としているわけですが…

このexpr型を使って構成される「別の代数型」というものを考えた時、単にexpr型とだけ宣言されると、表現としてイマイチになることがあるのですよね。

例えば次のようなAbstract Syntax Treeを定義したとき…

type ast = 
  | Value of expr
  | IfExpr of expr * expr * expr

なんかIfExprexpr * expr * exprの部分の表現力が乏しくないですか?

exprが3つ並んでるんですけど、それぞれがどういう性質のexprを要求しているのか、よくわからないですよね。

もしこのIfExpr

IfExpr of (bool値のexpr) * (int値のexpr) * (int値のexpr)

みたいなニュアンスで型を制限できたら、安全だし、わかりやすくないですか?

これを、まさに可能にするのがGADTというやつらしく。

ちなみにGADTというのは、Generalized Algebraic Data Typeの略で日本語なら「汎用代数型」とでも訳すのでしょうか?

実際にexprとastをそれぞれGADTを使った型宣言(expr'とast'とする)に書き直すと、こういう感じになります。

type _ expr' = 
  | Gint: int -> expr' 
  | Gbool: bool -> expr'

type _ ast' = 
  | GValue: int expr'
  | GIfExpr: bool expr' -> int expr' -> int expr' -> int expr'

普通の代数型だとofが来る部分に:が来て、右側が「型の関数」みたいな記述になるのが特徴です。

で、構成される代数型に型の制約を付けたい場合はtype _ expr' = ...みたいに宣言します。

アンダースコアの部分は「代数型に何かしらの型アノテーションがつくよ」みたいなニュアンスなんでしょうか?

こうやって作られたGADTを使うと、

let statement_by_gadt : ast' = GIfExpr (Gint 10) (Gint 20) (Gint 30)

は、IfExprの一つ目のexpr'が「bool expr'」じゃないので、コンパイルの段階でエラーになります。

一方で、GADTじゃない、普通の代数型のastを使った場合、次の

let statement_no_gadt : ast = IfExpr (Tint 10) (Tint 20) (Tint 30)

は(型の上では)エラーではありません。

IfExprを構成するメンバーは全てexpr型と規定されているだけなので、型の上では正しいからです。

これを構文エラーにするには、evalするときにundefned patternとして、型エラーを自前で書くしかないわけですが、GADTで制約していればコンパイルの段階でエラーにしてくれます。

jingoo v1.2.5 release

jingooのv1.2.5をリリースしました。

ひょんなことからjingoov1.2.4にバグ(割りとでかい)を見つけてしまい…緊急でリリースしました。

旧バージョンを使用中の方は、アップデートすることをおすすめします。

バグの詳細

具体的なバグは何かというと、オブジェクトをドットで展開する際に、対象が以下の様な入れ子のオブジェクトだったとき、evalが失敗して例外が投げられていたことです。

{{ user.image.filename }}

原因

原因はASTの評価処理で、ドットでプロパティにアクセスする評価式を、次のようなパターンマッチでのみ受け取っていたからでした。

| DotExpr(Ident(name), Ident(prop)) ->
  jg_obj_lookup_by_name env ctx name prop

オブジェクトのドット展開は左結合で、右にドットが2つ以上続く場合は左辺が再帰的にオブジェクトを返すはずですが、DotExprの評価式で左辺がIdentのみの評価しかしていなかったので、左辺がobjectを返すようなASTだったときにマッチするパターンがなく、SyntaxErrorがthrowされていたわけです。

今までこれに気づかなかったのは、単に入れ子のオブジェクトを扱っていなかったからなんですが、今回キャラクタ機能というのをリリースした際に、キャラクタオブジェクトの画像オブジェクト、という入れ子のオブジェクトを展開する必要があって、初めてバグに気づきました。

修正

対応自体は簡単で、パターンマッチのケースを一つ追加するだけで大丈夫でした。

| DotExpr(Ident(name), Ident(prop) ->
  jg_obj_lookup_by_name env ctx name prop

(** 左辺がオブジェクト *)
| DotExpr(left, Ident(prop)) ->
  jg_obj_lookup env ctx (eval_expr env ctx left) prop

おまけ

久しぶりのソースだったので「思い出せるかなあ」と憂鬱だったのですが、OCamlってコンパイラに型エラーで怒られているうちに、徐々に思い出すんですよね。

改めて型システムは偉大だなあ…などと思いました。

OCaml用の麻雀ライブラリ

フォルダ整理していたら、二年ほど前に書いたOCaml用の麻雀ライブラリが発掘されました。

しまっておくのも無駄だし、バックアップも兼ねてgithubに上げておくことに…

https://github.com/tategakibunko/ocaml-mjlib

一応、ゲームやプレイヤーなども含めて抽象化されている(っぽいけど忘れた)。

ちゃんとユニットテストも付いてるから、それなりに動くんだと思います。

縦書き文庫では、ろくにテストもせずに上げ、苦情ドリブンで対応する僕ですが(個人的にソーシャルデバッグと呼んでいる)、 こういうロジック系のライブラリでは流石にユニットテストぐらいは書きます。

ちょい見る限り、どこか抜けはあるかもしれませんが、日本ルールの役は一通り得点計算できてる(はず)。

ちなみに、よくブログ記事で麻雀の処理を書いてみた、みたいなので、アマタ、シュンツ、コーツをパースして「はい、おしまい」みたいな人をよく見かけますけど、それって麻雀全体の得点計算ロジックの1割にも満たない部分ですからね。

というのも、麻雀って、手元にある14牌の状態だけで判断できる役とか得点なんて殆どないのです…

得点を正しく計算しようと思ったら、とにかく色々なコンテキストを検証する必要があるので。

まずそもそも手元の14牌だって、最後の一枚がツモなのかロンなのかってので区別する必要がありますよね。

ツモかつ喰いがなければ「ツモ」なる役が付くわけですし。

さらにプレイヤー同士の席順で判断する必要のある役とか点とかルールもあるから(リーチ一発とか、ダブロンとか、チーはカミチャだけとか)プレイヤーを抽象化する必要もあるし。

面子だって、鳴かれたものなのかどうかで府の計算も変わるし、役の扱いも変わってくるし(じゃなきゃ、ただのトイトイがスーアンコになってしまう)。

また上がる前の「待ち」がどうだったかによって付く役と付かない役とか符もある。

リャンメン待ちじゃなけりゃピンフにならないし。

リャンメンじゃないならないで、そういう場合は待ちには符がついて得点が変わるし。

また場風で決まる役もあるから(風牌とかダブトン、ダブナンとか)、ゲームの進行状況も抽象化しないと正しい得点計算が出来ない。

他にも裏ドラとかリンシャンとかハイテイとかチョンボとか…まあ色々です。

とにかくゲームの状況(コンテキスト)で決まるものが多すぎるので、必然的に色々なものを抱え込んで抽象する必要があるわけです。

例えばカレンダーを扱うライブラリだってそうですよね。

単に暦とか言ったって、見る観点が色々とあるので。

時間として扱うのか、日付として扱うのか、それともある日付を基準として数えた現在までの秒数として扱うのか。表示フォーマットだって、国ごとに色々あります。

麻雀ライブラリもそういう感じです。

ちなみに麻雀ゲームは過去に色々な言語で作ったことがあるのですが(個人的な趣味です)、 一番楽に書けたのはOCamlだったと思います。

過去の組み合わせは、サーバー/クライアントの組み合わせで

とかです。

まあ経験値が上がったぶん、最新のものが楽になってるのは当然かもしれないですけど。

これ以外には、Haskellの自習用で作った簡易麻雀ライブラリ(hml)もありますが、HaskellOCamlと同じぐらい書きやすかったかもです(特にShowって型クラスが便利だった)。

ちなみにこのライブラリ、最近になって妙にforkされているのですけど、Haskellの手習いとして作ったものなので純粋にヘタクソだし、そもそも大きな山の一割ぐらいしか上っていないので「いいんかな?」という感じです。