subtype 多相, never, union を有効活用していこうな

まずは MonadPlus ばんじゃーいとこういう感じに mzeromplus を定義してみる. mzero が関数になっているのは TypeScript ではこうしないと (関数以外の値を) パラメータ多相にできないため. ちなみに Parser<A>A は結果の値の型で, Parser はこれについて共変.

declare const mzero: <A>() => Parser<A>;
declare const mplus: <A>(parserA: Parser<A>, parserB: Parser<A>) => Parser<A>;

するとこういう感じで, 本当は p2Parser<T> になってほしいのだけれど, 明示的に型パラメータを渡したり注釈を書かない限りは mzero を適当にインスタンス化するのが優先されて Parser<{}> になってしまう.

enum T { } // なんでも良い
declare const parser: Parser<T>;

const p1 = mplus(parser, mzero()); // : Parser<T>
const p2 = mplus(mzero(), parser); // : Parser<{}> ← えっあのちょっと

というわけでパラメータ多相をやめて, かわりに subtype 多相を使ってこんなふうに never と union (|) を使って定義しなおす.

declare const mzero: Parser<never>;
declare const mplus: <A, B>(parserA: Parser<A>, parserB: Parser<B>) => Parser<A | B>;

するとめでたく両方 Parser<T> になります. 任意の T に対して T | never = never | T = T なので never は消えてなくなる. ついでに mzero を使うのにいちいち関数呼び出しをする必要もなくなって便利.

const p1 = mplus(parser, mzero); // : Parser<T>
const p2 = mplus(mzero, parser); // : Parser<T>

かわりに mplus の引数として別々の型を返す Parser を渡して良くなってしまっているわけですが, だからといって結果の型に不整合が出たりするわけではないので特に困ることはないでしょう.

何の話: https://github.com/susisu/loquat/blob/master/packages/simple/index.d.ts

パーサコンビネータライブラリを更新した (2 年ぶり 2 回目)

2 年に一回更新することでおなじみの (?) JS 製パーサコンビネータライブラリ loquat の v3 をリリースしました.

github.com

変更点の詳細はここには書かないのでリポジトリを見てください. どうせ誰も使っていないでしょうし...

以下は雑な話題です.

TypeScript

現代において TypeScript 向けの型情報が提供されないならば即ち使いにくい, と言っても過言ではないくらいでしょう. ということで型情報を含んで単純化したインターフェースを提供することにしました.

www.npmjs.com

今回の更新の主な内容はこれで, ある程度 TypeScript フレンドリーになるようにインターフェースをいくらか変更しています.

TypeScript で書き直さないのですか

いいえ. 正直 TypeScript で書き直すメリットが無いです. ただし決して動的型付けこそ至高みたいな意味ではないです.

TypeScript の型システムはある面 (例えば古くからある JS の混沌としたインターフェースに無理やり型をつける) ではとても強力なものの, 一方で Haskell, Scala, OCaml などと比較すると型システム自体や型推論においてかなり非力な面が目立ちます. loquat の元ネタは Parsec なので, 機能を完全に保ったまま真面目に型をつけようとすると Haskell か, おそらく ScalaOCaml 相当の機能 (associated type や dependent object 的なもの, あるいは functor, さらに多相な値とか) が必要になってきます. またそれが実現したところで型推論が貧弱では, 開発している私も, さらにはユーザーも冗長な型定義を書く羽目になり大変不便です.

完全に型をつけるのを諦めた上で TypeScript で書くこともできますが, 単に any だらけになって読みづらい上に特に安全性も増していない, みたいな状態になりそうだったのでやりませんでした. また機能を削ることで綺麗に型をつけることもできますが, これも技術的面白みが無くなるのでやるつもりはないです.

上で書いた TypeScript 用の型定義は, まさに本来提供している機能から難しい部分を削っています. それでもきっと 99% のユーザーにとっては十分なんじゃないでしょうか. 知らんけど.

パフォーマンス

ここで雑に計測してます.

github.com

たぶん世にある JS 製パーサコンビネータの中では最速で, 前回のバージョンから特に劣化はしていません. いい感じに書けているのか Node.js (V8) のバージョンが上がるとちょいちょい相対的に速くなります. parsimmon はバージョン上げたらエラーメッセージが豪華になったらしく勝手に遅くなっていきました.

monorepo

以前からプラグイン方式を採用しているためにパッケージが複数リポジトリに散らばっており, 開発中に yarn link などするのも大変だったので, 今流行りの Lerna ってやつで monorepo にしてみました. なんかまだあまりこなれた開発フローを構築できていないですが, 使っているうちにきっと慣れていくでしょう. というか使わなければ慣れることもないわけで.

愚痴

JavaScript でパーサコンビネータを書くのにあたって技術的課題はいくつかあって, これまでに都度解決策を考えてきました.

で, 先日これらをまったく解決していないライブラリが「TypeScript で書かれていて使いやすい」と受け入れられているのを見て, まあユーザー観点から見たら仕方がないのかなという気持ちはありつつも, かなり残念に思いました (これは相当に丸めた表現です).

今回単純化したインターフェースと TypeScript の型定義を用意したのはこの件へのレスポンスです. なんというか, なんかいろいろですね.