kmizuの日記

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

型クラスをOrderingを用いて説明してみる

ちょっと今日は疲れてるので、表題の件について、簡単に書いてみる。私の経験上、型クラスにおける、最も多い誤解は、(Javaとかにおける)インタフェースのようなもの、というもので、これはかなり多くの人にみられるように思う。

まず、そもそも、何故そういう勘違いが生まれたかを考えてみると、「インスタンス」「メソッド」といったオブジェクト指向言語にもある用語が使われていること、また、両者とも「操作の集合を提供する」という特徴があるためではないかと思う。しかし、根本的な違いがある。一番大きなものは、型クラスは(一般的には)レシーバ(thisといってもselfといってもなんでもいいが)とそれに付随する状態が基本的に存在しない、という点だ。

この点について、Scalaの標準ライブラリにおいて、両者の違いを説明するのに格好の例がある。OrderingOrderedだ。混乱を避けるために先に説明しておくと、両者ともScala言語上の仕組みとしては、trait(≒実装ありインタフェース)なのだが、Orderingは、this(の状態)を使わずに全てのメソッドのプロトコルが定義されているのに対して、Orderedはthisと引数との比較をするためのプロトコルが定義されている。

まず、比較する対象のクラスPersonを定義する。比較がしたいだけなので、これはage: Intをフィールドとしてもつcase classとして実装することにする。以下のようになる:

case class Person(age: Int)

OrderedOrderingPersonに関する実装を、それぞれ書いてみる。まずはOrdering[Person]だ。

import scala.math.Ordering

implicit object PersonOrdering extends Ordering[Person] {
  override def compare(x: Person, y: Person): Int = {
    if(x.age < y.age) -1 else if(x.age > y.age) 1 else 0
  }
}

ここで、どっかで見たことあるような…と思った人は鋭いが、これは、java.util.Comparator<T>とほぼ同じだ。違いはどこかというと、Orderingはimplicit parameterとして与えられると、(まさに)implicitに検索されて、コンパイラによって追加の引数として与えられるのに対して、java.util.Comparator<T>は呼ぶ側がexplicitに与えなければいけないという点にある(これは不正確で、実際のところ、java.util.Comparator<T>がimplicit parameterとして宣言されれば、それは型クラスとして振る舞う。正しい説明としては、java.util.Comparator<T>はexplicitに与える「ため」にもっぱら使われる、ということになるだろう)。さて、java.util.Comparator<T>のような使い方は、オブジェクト指向言語では一般的にStrategyパターンとして知られているが、型クラスはStrategyを暗黙に渡すための言語機構を用意しているということができる。実際のところ、これで説明はほとんど終わりだ(コンパイラがどうやってStrategyを見つけるのかという点を説明していないが)。

次に、Orderedについて考えてみる。これは次のようになる:

import scala.math.Ordered
case class Person(age: Int) extends Ordered[Person] {
  def compare(that: Person): Int = {
    if(this.age < that.age) -1 else if(this.age > that.age) 1 else 0
  }
}

ここで、元のPersonのクラス定義を書き換えている、というか、書き換えないと実用にならないという点が一つの問題になる。定義を書き換えない方法として、ラッパーを作る、例えば次のようにすることも可能ではある:

import scala.math.Ordered
case class PersonWrapper(person: Person) extends Ordered[Person] {
  def compare(that: Person): Int = {
    if(this.person.age < that.age) -1 else if(this.person.age > that.age) 1 else 0
  }
}

しかし、ちょっと考えればわかるが、たとえば、Personのリストをソートしたいのに、そのために全要素をラッパーに変換する、という操作を事前に行うのは、オブジェクトの確保コストが馬鹿にならないし、とても面倒でもあり、およそ非実用的だ。なので、Orderedをまともに運用したければ、ほとんど必然的に元のデータ型の定義を書き換える必要に迫られる*1

これが、通常の、(thisが)状態を持ったインタフェースをベースにした解決策の問題だ。一方、Orderingを使った解決策では、事前に比較関数を準備しなくても、後付けで比較方法を提供できる。通常のStrategyパターンでは、Orderingを明示的に渡す必要があるが、型クラスを用いるとそこも暗黙的にできる。たとえば、このような感じになる:

List(Person(1), Person(5), Person(3)).sorted // => List(Person(1), Person(3), Person(5))

まとめ

  • 通常のインタフェースベースと型クラスベースの解決策の最大の違いは、レシーバの状態を必要とするかしないか(正確には、型クラスにおいて、レシーバは登場しないが、オブジェクト指向言語では全てのメソッド呼び出しがレシーバを持つため、オブジェクト指向言語において型クラス相当の機能を提供する場合、レシーバを「使わない」という形になるはず)
  • 型クラスベースの解決策では、ある種の型に対する操作の集合を「後付けで」与えることができる
  • 型クラスのインスタンスを作ることは、Strategyパターンにおいて具体的なStrategyを定義するようなもの`
  • どのようなStrategyが引数に与えられるかをコンパイラが推論できるようにしたものが型クラスである

言い訳

Wadlerらが提案した元々の型クラスとは、実際の裏側の仕組みや、型推論との関連において異なる。また、オーバーロードを一般化したものという元々の提案にあった視点についても、うまく説明に取り込むことができなかったため、あえて書いていない。型クラスの継承についてもはしょった。ただ、状態を保たない操作の集合を提供するものが型クラスである、という視点を得ることができれば(この点でつまづく人が多いように思う)、型クラスのその他の点について理解することもそれほど困難ではないと考えている。

*1:実は、Scalaの機能の一つであるimplicit conversionを使えば面倒さに関してはなんとかすることも可能ではあるが、オブジェクト確保のコストに関してはどうしようもない

シンフォニック=レインというゲームをお勧めしてみる(注意:ネタバレは見ないで)

このブログでオタク系の話題を書くのは、もしかしたら初めてかもしれませんが、表題のゲームのSteam版が発売されるというニュースを見つけたので、このゲームが好きな人間としてこれを機会に布教しとこうかと思って筆をとりました。

シンフォニック=レインは、2004年3月に工画堂スタジオから発売された、全年齢向けゲームです。ジャンルはPCからはほとんど姿を消した、いわゆるギャルゲ(健全)です。普通の紙芝居形式のギャルゲと違う特徴としては、ミュージックアクションパート、という、音符に対応したキーボード上のキーを適切なタイミングでタイプするゲームが要所要所にあることですが、音ゲー苦手な人用に完全スキップも可能になっています。

作曲・作詞を担当した岡崎律子氏がこの作品が出た2か月後くらい後にお亡くなりになってしまったため(胃ガンだったそうです。ゲーム中の曲も闘病中に作られたものです)、ゲームには興味ないけど岡崎氏のファンが注目するとかいうこともありました。

このゲーム、爆発的な人気こそ出なかったものの、コアなファンがずっといる不思議なゲームで、これまで、

  • 初回限定版/通常版
  • 愛蔵版
  • 普及版
  • Steam版(2017/06発売)

と版を重ねてきています。なんで根強いファンがいるかというと、一つにはシナリオの出来の良さ、二つ目は楽曲自体が物語と密接にリンクしているという巧さがあるのかなと思います。

あらすじを簡単に紹介しておくと、主人公のクリスはフォルテールという架空の楽器を演奏する才能が生まれつきありました(劇中では、魔力とされていますが、魔力とは何かが劇中で明かされることはないので、単に楽器演奏の才能があったと言い換えていいです)。クリスには幼馴染の双子姉妹アリエッタとトルティニタがいるのですが(ありがち設定ですね)、姉のアリエッタには歌とか音楽の才能がなく、妹のトルティニタには反対に歌の才能がありました。

結果として、思春期になって、クリスはアリエッタと恋人になるのですが、音楽家への道を志すために、音楽の街と呼ばれるピオーヴァの音楽学院にアリエッタの妹トルティニタと共に通うことになります。主人公たちの家とピオーヴァは電車で1日以上離れているので、普段のやり取りは手紙のみです(電車はあるのですが、電話はない世界観です)。

で、それから3年近くが経って、卒業まで残り2か月程度。卒業演奏のための曲を作る必要と、歌を歌うパートナー(同性でもいい)を早く見つけなければいけないのですが、クリスはやる気ナッシング。ピオーヴァの下宿に引っ越してから何故か見えるようになった妖精フォーニ(他の人には見えないので幻覚の恐れあり)とじゃれあいながら自堕落な生活を過ごしています。

そんなクリスに対して、トルティニタはさっさと自分あるいは他の誰かとパートナーを組ませようと色々根回しをしたりするのですが、それもなかなかうまく行かず、さて、どうなるんでしょうね?というところで物語がスタートです。

嘘は書いていませんが、微妙に興味を引くようにあらすじの状況を言い換えてみました。この辺で何かピンと来るものがあった人がいれば、6月に発売のSteam版をぜひ買いましょう。

既にロットアップして久しいですが、中古でもまだそこそこ売っているはずなので、今すぐやりたい人は中古買いましょう。普及版が、だいたい、追加要素全部込みなのでお勧めです。

以下、ネタバレ注意のため反転(ただし致命的なことは書いてないので、読んだ上で状況を考えながら楽しむのもありですし、興味わかなかった人が読んでみて興味がわくこともあるかもしれないので、興味ない人はむしろ積極的にみるの推奨です)。

このゲームには、自分が思うところでは、中盤までに3つの「謎」あるいは「不審な点」があることに多くのプレイヤーが気づくと思います。そのうちの1つはたぶん割とわかりやすいので、確信を持てるのは後半になるにしても、序盤でん?と思うだろうと思います。

残りのうち1つは、ある程度物語が進んできて、最初の一つの点について疑問に思ったあたりで、必然的に、こっちの点についても気づくと思います。ただ、この点についてのみ若干ファンタジー要素があり、それを徹底的に排除した作品としてとらえると気づくのがだいぶあとになるかなとも思います。

最後の1つは常識的に考えて、まず無理というタイプですが(それで、最後の一つがどういうタイプの「謎」なのかわかった人もいると思います)、物語中に気づくためのヒントは一応提示されており、謎が明かされてからみると、登場人物たちの一見よくわからない不審な行動にちゃんと一貫した筋が通ってるのが2度読んでわかるあたり、割と良い作品である証だと思います。裏がわかったら冷めるとかいうタイプではなく、裏がわかったあとで、最初から読み直してみると全く違った風に読めます。

というわけで、普段技術ネタばかりのこのブログですが、珍しく趣味的なゲーム紹介をしてみました。全体的にダウナーな雰囲気のゲームで、決して読んでいて楽しい作品ではないですが、それでも面白いのは確かなので、是非、買ってみてください!

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文字クラス * アンカー文字列の強化 * パーズエラー時のメッセージを親切に * 引数付き規則のサポート