kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Rubyっぽく楽にIO処理を書けるライブラリScaruby 0.3をリリースしました

Scalaは標準IOライブラリが非常に貧弱な言語です。scala.ioはまともに使えるものではありませんし、JDKのライブラリのIOを使うのも面倒です。そこで、サードパーティのIOライブラリを使うことになります。そこで、いくつかサードパーティのIOライブラリを見てみたところ、いずれも自分のやりたいことをぱっとできるように思えなかったので、自分で作ってみることにしました。

なお、better-filesは、自分の要求に一番近かったですが、java.ioやjava.nioがインタフェース部に露出しているのがあまり好みではありませんでした。

そこで、

  • java.ioがインタフェースに露出しない
  • Rubyのように簡単なIO処理が簡単に書ける
  • リソースの後始末は可能な限り簡単に
  • 多数のリソースをネストせずに扱える
  • オーバーロードをしない(!)

を目標として、新しいライブラリScarubyの開発を始めました(Scala + Rubyというネーミングです)。これまでで、とりあえず簡単なファイルI/OやURLからのテキスト読み込みなどが動作するようになったので、公開することにしました。

サンプルプログラムのいくつかは以下のサイトに置いてありますが、ドキュメントの充実はまだまだこれからということです。

github.com

たとえば、次のようなプログラムが動作します。

import com.github.scaruby._

val content = SFile.read("file.txt")
import com.github.scaruby._

val input = "Hello, World!"
val encoded = SBase64.encode64(input.getBytes("UTF-8"))
val decoded = new String(SBase64.decode64(encoded), "UTF-8")
assert(input == decoded)
import com.github.scaruby._
val content = for { 
  r <- SURL("https://srad.jp/").openReader
} r.readAll()
println(content)
import com.github.scaruby._
println(SURL("https://srad.jp").read())
import com.github.scaruby._
SFile("file.txt").read()
import com.github.scaruby._
SFile("output.txt").write("HogeFuga")

今後の予定としては、

  • JSONエンコーダ/デコーダの追加(他に依存しないもの)
  • その他のファイルフォーマットの対応

などを考えています。要望があればIssueに上げていただければ励みになります。

Scalaに関する誤解と事実を語る

TL;DR

  • 世間のScalaに関するイメージは、昔のままであることが多い
  • 昔のままどころか、最初から間違ったイメージを持たれていることも多い
  • 実際には、既に解決されている問題は多々あるし、改善に向かっていることも多い
  • プロジェクト管理の問題を言語に押し付けているケースもある

はじめに

自分が最初にScalaに触れたのが2005年(Scala 1からカウントした場合)、あるいは2007年(Scala 2以降からカウントした場合)と、Scalaとの付き合いも結構長くなってきましたが、その間に

  • Typesafe社(現Lightbend社)の設立
  • 実質標準ビルドツールとしてのsbtの確立
  • ライブラリのバイナリ後方互換性に関するポリシーの策定
  • 公式ScalaイベントScala Daysのはじまり
  • Play 2 Frameworkの登場
  • Scala Center発足
  • その他色々

がありました。この間、Scalaコミュニティ自身も変化してきており、昔のScalaに対する印象が今のScalaに対しても正しいとは言えなくなっています。そこで、Scalaに関する「誤解」(とは言い切れないのですが、現在はあてはまらないもの)と「事実」についていくつか語っておこうと思います。

誤解1:Scalaコンパイルが遅い

Scalaの欠点の筆頭に挙げられるコンパイル速度の問題ですが、誤解ではありません。たとえば、Javaコンパイル速度と比べると遥かに遅いですし、Scalaプログラマはこのことをよくネタにするくらいです。

ただし、たとえば、Kotlinがコンパイル速度がScalaより速いといった風聞は必ずしも事実に基づかないものです(KotlinとScalaコンパイル速度比較についてはこちら)。

また、これは多くの人が犯している過ちだと思うのですが、Scalaの標準ツールであるsbtは立ち上げたまま様々なコマンドを発行して利用することが前提のシェルでもあり、起動したままコンパイルを続けるとコンパイラの速度は倍以上高速になります。このことを知らないまま、毎回sbtを立ち上げなおしていると、せっかくJVMのwarmupによって高速になったコンパイル速度を無駄にしてしまうことにつながります。

なお、Scala 3のコンパイラになることが決まっているDotty

github.com

は現在のscalacに比べて2倍程度高速らしいです(これについては実測していません)

また、scalacのコンパイルを並列化するというアプローチで、Triplequote社がHydraという並列Scalaコンパイラを提供しています。Hydraは現在はOSSでなく、限られた顧客にのみ提供されているようですが、コンパイラをうまく並列化することによって、10コアのサーバマシンにおいて、大規模プロジェクトのビルドがおよそ6.3倍高速したとのことです。これが典型的なデータであれば、今後コア数がさらに増えることが予想されるなかで、ScalaのビルドもPCの多コア化にともなって高速化することが期待できそうです。

もし、HydraがOSS化されることがあれば、Scalaの(コンパイル)遅い、は過去の話になるかもしれませんね。

誤解2:Scalaは(実行速度が)遅い

これは紛れもない誤解、というか、間違い、なので、はっきりとそのようなことはないと言っておきましょう。

scalacのコンパイルしたコードを逆アセンブルしてみた人ならわかると思いますが、scalacの吐くコードというのは極力JVMの命令を直接呼び出すようになっています。たとえば、基本型がオブジェクトであるといったことから、基本型を使うのにもオブジェクト生成が伴うイメージを持っている方もいるようですが、基本型がオブジェクトであるというのは単にそう取り扱って問題ないように言語が設計されているというだけで、基本的にはJVMの基本型演算命令をそのまま使います。

ですから、Javaで速度的に問題ないケースであれば、Scalaを適用して速度的に問題はありません。

誤解3:Scalaでは記号メソッド(あるいは演算子オーバーロード)が濫用されている

これは、昔、Scalaを紹介するのに、foldLeftの別名である/:メソッドが使われたことの影響もあると思います。また、昔のScalaによるHTTPクライアントライブラリdispatch(-classic)において、記号が非常に多用されたことも影響しているでしょう。

現在の、というか、数年前からScalaコミュニティでは、記号メソッドの濫用については抑制的になっており(のはず)、むやみに記号メソッドを使うことには慎重になっています。たとえば 新しい方のdispatchでは記号メソッドは大幅に減っています。

誤解4:implicitは濫用されがちな機能であり、コードの可読性を下げるので良くない

これも昔のScalaコミュニティにおいてそういう傾向があった、という事実がベースにあるのだと思われます。しかし、現在は、既存の型と新しい型の間の変換を行う用途のimplicit conversionは既にアンチパターンとみなされています。たとえば、Scala 2.8から標準ライブラリにはJavaConversionsという、JavaのコレクションとScalaのコレクションを相互に暗黙的に変換するライブラリがありましたが、Scala 2.12ではこれは非推奨になり、その代わりに、asScala()asJava()などとして明示的に相互変換するための`JavaConvertersを使うべきということになっています。

いわゆるimplicitと呼ばれる機能群の中で現在も必要とされているのは、

  • 型クラス(implicit parameter)
  • 拡張メソッド(implicit class)

の主に二つですが、拡張メソッドは最近の言語ではよく見られるものですし、型クラスもその有用性については(Haskellなどの言語において)十分に実証されています。型クラスについても濫用は可能であるという批判はできますが、それをするにはまず型クラスを理解する必要があるでしょう。

誤解5:ScalazやCatsは、圏論とかなんか難しい知識が必要らしくて怖い

おそらく、ScalazやCatsの位置づけについて誤解があると思われるのですが、これらは別に準標準ライブラリという程ではなく、あくまでもサードパーティのライブラリに過ぎません。使いたくなければ使わなければいいだけの話です。既に導入されたプロジェクトについていくためには、ScalazやCatsを勉強する必要があるでしょうが、それは何のライブラリであっても同じことです。

なお、ScalazとかCatzのライブラリ名に圏論の影響があるのは間違いありませんが、実際に使うときにそれらの知識が必要かというと必ずしも必要ないと思います。

誤解6:Scalaは、手続型プログラミングも可能だと宣伝しているが、実質的には関数型プログラミングのスタイルで書かなければいけない!騙された!

これは誤解かどうかちょっと微妙なところです。というのは、実際には、Scalaで手続型プログラミングをするためのライブラリも多くあるのですが、関数型、とりわけ純粋関数型アプローチに基づいたライブラリが目立つという傾向はあり、そのため、このような感想が出てくるのかなと思います。あまり関数型関数型したものを避けたい場合、The Scala Library IndexというScalaライブラリ専用の検索エンジンで探してみると良いでしょう。

ただ、いくら、Scalaで手続型プログラミングが可能であるといっても、map一発で済む処理をいちいち可変コレクションをループで回していると、さすがにつらい、というか、Scalaで書くメリットがあまりないので、最低限、Scalaのコレクションライブラリは使えるべきだと思います(これは、Scalaに限らず、最近の言語はだいたい汎用コレクションに対する高階メソッドが標準であるので…)。

不変コレクションを多用することで、そのメリットを享受しつつも、DBとのやりとりなどの外界とのインタラクションについては無理やり関数型的に扱わずに、素直に手続型で扱うのが良いというのが現在の自分の意見です。

誤解7:Scala後方互換性を重視しない(あるいは無視する)

これに関しては、まず、ソース互換性(.scala後方互換性)とバイナリ互換性(.classの後方互換性)があることを認識する 必要があります。

その上でいうと、Scalaはソース後方互換性をなるべく維持するように進化してきました。Scala 2.10で コンパイルできるコードは(非推奨なライブラリを使っていなければ)、Scala 2.12でもほとんどのケースでコンパイルを通ります。というわけで、ソース互換性をScala開発チーム含むコミュニティはとても重視しています。

バイナリ互換性の問題について論じましょう。その前に、Scalaのバージョンに関して、

  • メジャー・バージョン: Scala 2.X.YのX(だいたい1年以上の周期でアップデート)
  • マイナー・バージョン: Scala 2.X.YのY(短いと1週間くらい、通常は数週間くらいのペースでアップデート)

です。この定義の上で、まず、同じメジャー・バージョンのScalaに対してコンパイルされたライブラリは、マイナー・バージョンが変わってもメジャー・バージョンが同じScala上では動きます。また、同じメジャーバージョンのScalaに対してコンパイルされたライブラリ同士を混ぜることも可能です。

ここで、「じゃあ、Scalaのメジャー・バージョンが上がった場合、そのためにライブラリを書き直さなければ、新しいScalaで使えないのか?」と思った方はいると思います。

その問題は皆無とは言えないのですが、よく使われているScalaライブラリは、sbtのクロスビルドという機能を使って、最低2世代、多いと3世代のメジャーバージョンに対してライブラリをビルドして対応しています。そのため、新しいScalaのメジャーバージョンが出たときの初期の頃こそ、対応ライブラリが出揃わないという問題はありますが(scala-library <- A <- B <- Cみたいな依存関係があると、Cは、AとBが対応してくれないと新しいScalaのメジャーバージョンに対応したものを出せないといったちょっと面倒な話はあります)、1ヶ月くらいすればだいたい落ち着くので、それほど深刻な問題であるとは思わないです。このとき、ライブラリのメンテナの作業としては、sbtの定義ファイル中の

crossScalaVersions

に、新しいメジャーバージョンを加えてライブラリをpublishするだけで良いので、それほど負荷が高い作業でもありません。Scalaの新しいメジャーバージョンが出た際、飽きたメンテナが自分のライブラリを更新しなくなることはよくありますが、多くの人に使われているものであればだれかがメンテを引き取ることもありますし、そうでなければそのライブラリの役割が終わった、ということだろうと思います。

このように、可能な限りソース後方互換性を保つことを前提とした上で、Scalaのメジャー・バージョン毎のバイナリクロスビルドによって、バイナリ後方互換性の問題に関してはかなり軽減することができていると言えます。もちろん、問題は皆無とは言いませんが、このことが致命的になったケースというのも思い浮かばないのが正直なところです。

なお、完全にバイナリ後方互換を保証するという選択肢もあったと思いますが、それではダメなライブラリはいつまでもダメなまま残ってしまいますし、JVMの新しい機能を利用できないというデメリットもあります。たとえば、Scala 2.12では、Java 8のinterfaceのデフォルトメソッドとラムダ式(というかinvokedynamic)を使うことで、クラスファイル数の削減と、ライブラリレベルでのバイナリ後方互換性を保ちやすくすることができるようになりました(Scala本体のバイナリ互換性を捨てることで、ライブラリのバイナリ互換性がかえって保ちやすくなるというのは面白いところです)。

一方、Kotlinでは、完全なバイナリ後方互換性を今のところ約束しているようですが(当面、Java 6で動作するバイナリを吐くことを明言しています)、これでは新しいJVMの機能は利用できないわけで、それでいいのかなあと思うところです。

たとえば、Kotlinでは、最新の1.1でも、Java 6相当のクラスファイルを出力しなければいけないせいで、実装ありのインタフェースが、実装ファイルとインタフェースファイルにコンパイルされます。これは、ライブラリを更新する際に、インタフェースにメソッドを追加するとバイナリ互換性が壊れてしまうことを意味します。バイナリ互換性を重視しているはずのKotlinで書いたはずのライブラリはバイナリ互換性を維持するのがより難しく、バイナリ互換性をKotlinほど重視していないはずのScalaで書いた(2.12以降)コードは、バイナリ互換性を保つのがより容易になるという皮肉な現象が起きるのです。

誤解ではない:Scalaは様々な書き方を許容するので、読みにくいコードが出来上がることがある

「誤解ではない」と書いた通り、そういう側面はあると思います。ただし、実際のプロジェクトではある程度のコーディング規約を定めるべきであることを考えると、コーディング規約を最初に一つ作る手間が削減できるくらいの意味しかないと思います。

また、今時のプロジェクトにおいて、メンバーがコミットしたコードに対してレビューがされるべき(建前じゃなくて普通にやりますよね?)ということを考えると、突然メンバーが変な書き方のコードを持ち込もうとしたら、それはコードレビューの段階でストップをかけるべき話なのではと思います。

これは、一昨年、とある会社がプロダクトコードをScalaからGolangに置き換えたという記事を見たときにも思ったことなのですが、たとえば <|*|>の意味がわからんという話なら、なぜそのコードをコードレビューではじかないのか、と(メソッド名からScalazのコード呼び出しであることは間違いないのですが、そんなコーディングスタイル全体に影響するライブラリを導入するのにちゃんと合意が取れてないというのはその会社の体制がgdgdなのでは、とか)。

おわりに

さて、だらだらと長い記事をお読みくださりありがとうございました。

自分がこれまでScalaについて観察を続けていて思ったことは、Scalaには今も昔も問題がたくさんありますが、問題が広く認識されれば、コミュニティもちゃんと解決に向かう健全さはある、ということです。最近の自分はあまりScalaコミュニティに対してコミットできていない気がしますが、今年はもうちょっとコミットしたいところです。ではでは。

Dottyで自作言語Klassicの処理系をビルドしてみる

つい先日、Scala 3になることが決定した次世代ScalaコンパイラDotty。このDotty、まだときどきコンパイラがクラッシュするなどのバグはありますが、Scala 2.11のライブラリを使うことができるので、Scala 2.11対応のライブラリやプロダクトを試しにビルドしてみることができます。

Dotty

に書いてある手順だけでは、Scala 2.11対応のライブラリを依存関係に含めることができないようです。Dottyで既存のソースをビルドするためには、次の手順に従う必要があります。まず、sbt-dottyプラグインをproject/plugins.sbtに含めます:

addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.1.0-RC4")

バージョンが0.1.0-RC4という微妙な番号なので、適宜読み替えてください。次に、build.sbtのscalaVersionとlibraryDependenciesを次のように修正します。

scalaVersion := dottyLatestNightlyBuild.get

これでdottyのnightly buildを取得してこられるようです。また、libraryDependenciesには、次のように、依存関係の記述のあとに.withDottyCompat()の呼び出しを入れます。

libraryDependencies ++= Seq(
  ("org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.4").withDottyCompat(),
  ("org.scalatest" %% "scalatest" %  "3.0.0").withDottyCompat()
)

また、project/build.propertiesではsbt.versionとして0.13.15を使うように指定します。

sbt.version=0.13.15

たったこれだけです。注意点として、現時点で(将来も?)structural typeはDottyで未サポートであるため、

def using[A <: { def close(): Unit}, B](resource: A)(f: A => B): B = try {
  f(resource)
} finally {
  scala.util.control.Exception.allCatch(resource.close())
}

のような形でLoan Patternを使っているライブラリは使わない形に書き換える必要があります。最後は、sbt run で実行してみます。

> sbt
> run -e "1 + 2 * 3 / 4"
[info] Running com.github.klassic.Main -e 1 + 2 * 3 / 4
2
[success] Total time: 1 s, completed 2017/05/04 9:28:25

問題なく実行できているようです。ただ、少し問題があって、sbt consoleを実行しようとすると、原因は不明なのですがNullPointerExceptionが発生するようです。ガッしたい気分ですね。

プログラミング言語作成ハンズオンを開催しました

connpass.com

今回開催したこのイベントは、私が学習用に作成したプログラミング言語nub

github.com

の文法や機能拡張を通じて、プログラミング言語処理系の作成の基礎について学ぶというものでした。

自分がこのイベントを開催したねらいは主に二つあって、

というものでした。準備には十分時間をかけたかったので、昨年秋から募集を始め、また、チューターとなってくれる 方も同時に募集しました。結果として、応募人数は十分過ぎる程、チューターも4名(うち2名は遠方(静岡と名古屋から!))来てくれる ことになりました。

チューターのno_maddoさん、yontaさん、long_long_floatさん、tetsuromさんの協力があったのは、特に、自分一人では質問に対応するのに 限界がある以上、大変助かりました。

反省点

さて、イベントを開催してみての手応えですが、興味を持ってがんがん手を動かしてくださった方も居た一方で、どう拡張すればいいかわからず 途方にくれていた方も居たように思います。後者のような人が出た原因の一つは、私が作成した学習用言語nubの仕様が学習用途としてはあまりにも リッチ過ぎたということがあったようです。この点は、nomaddoさんからも事前に指摘をもらっていたのですが、nubは

  • 変数宣言
  • 四則演算式
  • 比較演算式
  • if式
  • while式
  • 関数のユーザ定義・呼び出し

などを備えており、そこからさらに拡張しようと思うと、ややジャンプが必要であったのでした。私は当初この問題については軽く考えていた、というより、 プログラミング言語処理系初学者の気持ちがよくわかっていませんでした。結果として、どう拡張すればいいかわからない参加者がある程度出てしまう結果 になったようです。

実は、最初の機能追加提案としては、文字列型のサポートを考えていたのですが、no_maddoさんらの指摘もあったため、軌道修正しました。そして、二項演算子を追加する 提案を、ライブコーディングを交えて行ったところ、ある程度は効果があったようです。このような指摘がなければ、イベントとしては独りよがりなものになっていたかもしれません。 大変ありがたいことです。実際、文字列型のサポートに関してはイベント期間中に出来た参加者もいたものの、どうやっていいのかよくわからない方も居たようです。

nubの当日の仕様としては、現在のものから四則演算の一部を削り、さらにwhile式や関数のユーザ定義を削るくらいでちょうど良かっただろうというのが終わってみての実感です。

気づいた点

今回のイベントの目的の一つは、プログラミング言語処理系初学者がどこでつまづくのかを知ることでしたが、これに関してはある程度知見が得られたと思います。いくつかわかった点がありますが、特に

  1. ベタとメタの混同(という言い方が正しいかは微妙だけど)
  2. (具象)文法定義は難しい
  3. 再帰的な解釈(文法やインタープリタにおける)は難しい

が印象的でした。

最初の点ですが、たとえば、ある構文木のノードが変数名を保持する変数iを持っていたとして(この名前は良くないのですが、それはそれとして)、この変数iと nubの実際のプログラムで使われる変数であるiとの混同をしている方が居ました(当たり前のことですが、それが悪いとかいう意味ではなく、そういう混乱があるのだという単なる感想です)

二点目。今回、文法定義にはANTLR V4を使いました。これは、現存するパーザジェネレータの中で最も使い勝手や能力(扱える文法の範囲の広さという意味で)が高いものだったからですが、それでも苦戦していた人は多かったようです。この点を踏まえて、懇親会では、具象構文を使わずに抽象構文木を組み立てて解釈するだけにすればという話も出ました。一理あるとは思いますが、個人的な趣味としては、また、今後の可能性としても、具象構文を自在に操る能力はあって損はないとも思います。

三点目。これはそもそも再帰自体が難しいということに通じるのですが、プログラミング言語処理系の世界では、ごく当然のような顔をしてあちこちに再帰があらわれます。これはおそらく、苦労して再帰を扱っている人にとってはかなりしんどいのではと思いました。

また、これは最初から面倒になることがわかっていたのですが、代数的データ型およびパターンマッチングがない言語でプログラミング言語処理系作成を教えると、回り道があちこちできるので大変よろしくないことです。これはまあ、言語人口の多さを考えてJavaを選んだときからわかっていたことなのですが、早く主流の言語に代数的データ型とパターンマッチングが入って欲しいものです。

おわりに

何はともあれ、イベントは無事終了し、自分はいくつかの知見を得られました。参加者も全員とはいかないとは思いますが、プログラミング言語処理系を学習するきっかけになった方がいると思っています。チューター陣の方々にはお手数をおかけしましたが、おかげさまで質問に対して手が回らないという事態にはならずに済みました。また次回、このようなイベントがあるかはわかりませんが、もしやるとしたら、今回の範囲を踏まえて、もっと初歩から一歩一歩丁寧にやる形のものにしたいと思います。

ではでは、おやすみなさいませ。

正規表現のようでそうでない文字列マッチングライブラリ PEGEX 0.3リリース

PEGEXを開発し始めたのが確か2010年の春頃でした。元々は、私の専門であるPEGに対してより正規表現ライクな記法をサポートしたものでした。それから6年、現在は既にPEGのセマンティクスはほとんど残っておらず、(おそらく)全ての文脈自由言語を扱えるような強力なマッチングツールに進化しました。

たとえば、PEGEXで、任意階層ネスト可能で、しかも開始タグと閉じタグが一致しているXMLのような文法、というのは次のようにして記述することができます。

#{E}$; 
E=<(?<tag>#{I})>#{E}*</\k<tag>>; 
I=[a-z]+;

実は、たとえば、Ruby正規表現は既に古典的な意味での正規表現を超えており、再帰呼出しによってこのような文法を表現することができます。ただ、正規表現の記法に無理やり拡張を入れたため、正規表現を超えた部分についてはあまり可読性が良いとは言えません。PEGEXでは規則を;で分割することで、読みやすい形でパターンを記述することができます。より一般には、BNFで書かれているような仕様、たとえば、メールアドレスなどはPEGEXで比較的簡潔に記述できるでしょう。

PEGEXについて詳しくは、以下のページを参照してください:

github.com

今後は速度の向上や機能面で充実させていくことでより実用的にしていきたいと思っています。

次以降のバージョンで予定している機能のリスト: * POSIX文字クラス * アンカー文字列の強化 * パーズエラー時のメッセージを親切に * 引数付き規則のサポート

歯の健康を守ろう

この記事は、健康Advent Calendar 2016 12/13の記事です。

皆さん、歯、大丈夫ですか?自分は最近、右上の親知らずが虫歯にかかっていることが判明して抜歯する羽目になりました。ついでに、虫歯の治療も同時にしたせいで、一週間、左側の歯だけで飯を食わなければいけないことになりました。固いものはまず無理なので、これから1週間麺類とかスープとかやわらかいもの中心の食生活になりそうです。つらい。

なお、こういう歯の治療に行くと必ずといっていいほど、「歯磨きをちゃんとしてください」といったことを言われるのですが、歯医者が考える模範的な歯磨きは面倒過ぎてやってられません。かといって、ずっと放置していると今回の自分のように知らない間に大きな虫歯ができているということになります。

そういう事態を防ぐには、思うに、定期的に歯のメンテナンスに歯科に通うのがいいのではないかと思います(数ヶ月に1回くらい?)。歯石なども取り除いてくれますし、虫歯が深刻になる前にわかるというメリットもあります。

というわけで、皆さん、定期的に歯科に通いましょう。

Java (8)によるパーザコンビネータライブラリ JCombinator 0.0.1をリリースしました

Javaには既にJParsecというパーザコンビネータライブラリもあり、あえて新しいものを作る必要はないかもしれません。

ただ、JParsecはイマイチ気に入らなかったので、新しいパーザコンビネータライブラリを作ってみることにしました。とりあえず、基本的なコンビネータはそろっているので、デバッグの容易さとか考えなければこれで割と複雑なパーザを組むことも可能だと思います。

github.com

今後は、より色々なパーザを書きやすくするためのコンビネータの充実と、パーズエラー時のメッセージをわかりやすくすることに力を注いでいきたいところです。

例として、四則演算をできる式(括弧を含む)のパーザは次のようにして書くことができます。

import com.github.kmizu.jcombinator.datatype.Function2;

import static com.github.kmizu.jcombinator.Parser.*;
import static com.github.kmizu.jcombinator.Functions.*;

public class ArithmeticExpression {
    private Rule<Integer> expression() {
        return rule(() ->
            additive().cat(eof()).map(t -> t.extract((result, __) -> result))
        );
    }
    private Rule<Integer> additive() {
        return rule(() -> {
            final Parser<Function2<Integer, Integer, Integer>> Q = string("+").map(op -> (Integer lhs, Integer rhs) -> lhs + rhs);
            final Parser<Function2<Integer, Integer, Integer>> R = string("-").map(op -> (Integer lhs, Integer rhs) -> lhs - rhs);
            return multitive().chain(Q.or(R));
        });
    }
    private Rule<Integer> multitive() {
        return rule(() -> {
            final Parser<Function2<Integer, Integer, Integer>> Q = string("*").map(op -> (Integer lhs, Integer rhs) -> lhs * rhs);
            final Parser<Function2<Integer, Integer, Integer>> R = string("/").map(op -> (Integer lhs, Integer rhs) -> lhs / rhs);
            return primary().chain(Q.or(R));
        });
    }
    private final Rule<Integer> primary() {
        return rule(() ->
            number().or((string("(").cat(expression())).cat(string(")")).map(t -> t.item1().item2()))
        );
    }
    private final Rule<Integer> number() {
        return rule(() ->
            digit().many1().map(digits -> Integer.parseInt(join(digits, "")))
        );
    }

    public void testExpression() {
        Parser<Integer> arithmetic = expression();
        arithmetic.invoke("100").onSuccess(s -> {
            assert ((Integer)100) == s.value();
        });
        arithmetic.invoke("100+200").onSuccess(s -> {
            assert ((Integer)300) == s.value();
        });
        arithmetic.invoke("(1+2)*(3+4)").onSuccess(s -> {
            assert ((Integer)21) == s.value();
        });
        arithmetic.invoke("1+2*3+4").onSuccess(s -> {
            assert ((Integer)11) == s.value();
        });
    }
}