読者です 読者をやめる 読者になる 読者になる

kmizuの日記

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

Klassic言語の現状のステータス

Klassic は自分が新たに去年くらいから開発し始めたプログラミング言語です。

まだ確固たる設計思想があるわけではなく、色々な構文や機能について試しに導入してみて、テストを書いて…みたいなのを繰り返してる 段階です。現状のKlassic言語の大雑把な特徴としては

  • ML多相に類似した型システム(ちゃんとした、というか、普通の型推論があります)
  • 静的スコープ(現在では当たり前過ぎますが)
  • 第一級関数(今では当たり前過ぎますが、組み込みの関数型があって、というあたりは、より、いわゆる「関数型言語」風味に寄せています
  • ヒアドキュメント
    • Rubyとかのあれですね。これは、ヒアドキュメントの構文をきちんと理解するための実験過程で入りました
  • スペースセンシティブなあれこれ
    • 色々なものをスペースセンシティブな、つまり、空白に意味を持たせてみると便利になるのではないか、という実験
    • スペースセンシティブかつ改行センシティブなリストリテラル
    • スペースセンシティブかつ改行センシティブなマップリテラル
    • スペースセンシティブかつ改行センシティブな集合リテラル
    • 改行センシティブな構文
  • cleanup式
  • Java FFI
    • これは、単純に色々な機能を実現しようとしたときに、あると便利なので暫定的に入っています

ML多相に類似した型システム

よーするに「普通の」型推論が行われるということです。「普通の」というのは単一化ベースの型推論で、(おそらく)型注釈 は書かずに全部推論できる(はず)という話です。

たとえば、

def fact(n) = if(n < 2) 1 else n * fact(n - 1)

は、ちゃんとfact : Int => Intと推論してくれます。

mutable変数があるのに、多相性に制限つけてないので、mutable変数を考えると 健全でなくなるという大昔に解決された問題までそのまま残っています(笑)。これは、当然ながら型システムの性質として好ましく ないので、既存の解決法を流用するか、独自になんかそれっぽい解決法を適用してみる予定です

ヒアドキュメント

説明はあまりいらないと思いますが、

println(<<TAG1 + <<TAG2);
tag1
TAG1
tag2
TAG2

という感じのがちゃんとパーズされて動きます(一行に複数のヒアドキュメント開始が持てる(これはRubyもそうなので真似しています)点がポイントです)

第一級関数

map([])((x) => x + 1)

無名関数の文法はScalaの影響をかなり受けています。型クラスとかまだないので、x -> x + 1xの使われ方から、Int => Intとして推論されます。普通です。

スペースセンシティブかつ改行センシティブなリストリテラル

この辺の文法的な実験、というのが実は自分が一番やりたかったことです。

[1 2]

[1, 2]

と解釈されますし、

[[1 2]
 [3 4]
 [5 6]]

は、

[[1, 2],
 [3, 4],
 [5, 6]]

と解釈されます。要素を改行で区切るのは、trailing commaの利点も受けられるし、構文ノイズも入らないし、「思った通りにパーズされる」ので、割と良さげです。

スペースセンシティブかつ改行センシティブなマップリテラル

リストリテラルと同じような文法をマップリテラルにも入れてみました、というお話。

val points = [
  %["x":1 "y":1 "z":2
    "x":1 "y":1 "z":3]
]

%[で始まるのがマップリテラルで、キー・値ペアが空白でも改行でも区切ることができます。これもまあ、割と思った感じにパーズされます。集合リテラルの文法はリストリテラルとほとんど同じなので省略。

改行センシティブな構文

最近の言語には割とありがちですが、改行が色々なものの区切りになることができます。ただし、純粋に改行が来ると区切る、のだと困るケースもあるので、その辺はパーザが頑張ってます(この辺をどう頑張るかは言語によって異なると思いますが、Klassicでは割と単純な方法を採用しています)。

基本的なケースとして、

val x = 1
println(x)

このようなのはOKです。

val x = 1
+ 2
println(x)

このケースでは、1の後ろの改行で、行が終わるので宣言の終わりだと解釈するため、構文解析に失敗しますが

val x = 1 +
2
println(x)

のケースでは、1 +を読んだ段階で、これは行継続を意図してるっぽいと判断して、改行は意味がないものとして扱います。

cleanup式

色々な式について、その式が評価後に必ず実行されるコード(ただし、式の結果値は捨てられる)をくっつけることができます。

mutable i = 0
while(i < 10) {
  i = i + 1
} cleanup {
  println(i) // 10 is printed
}

後始末構文をもっと軽量にしてみたらこんな感じかなという割とどうでもいい実験です。

Java FFI

普通に、ScalaとかKotlinとかっぽい感じでJavaのメソッドが呼べます。

val newList = new java.util.ArrayList
foreach(a in [1, 2, 3, 4, 5]) {
  newList.add((a :> Int) * 2)
}
newList

ここで、:>は型キャスト式です。昔の時点で必要だったのでそのままコピペしてみたのですが、今のバージョンでは既に要らない気がします。

こんな感じで、今のところ、構文的な部分以外は何も特別ではないのですが、今後は言語機能上の実験も色々やっていきたいなと思うところです。今後の方向性として、structural type systemをまず入れる予定で、それに、open classを加えて、メソッド拡張を含めた場合にも型が推論されるような感じにするのがまずあります。

なお、構文はかなりScalaの影響を受けています(やっぱり今の自分にとって書きやすい構文の方に寄せたくなる)。

型クラスの真の力を見せる

昨日、

kmizu.hatenablog.com

という記事を書いたわけだが、その後、今日、型クラスに関する議論が一部で(?)盛り上がっているようだ。それは型クラスじゃなくても実現できるのでは、いや、やっぱりインタフェースのようなものと思っていいのでは、などなど。

今回の記事では、型クラスじゃないと実現が著しく困難であると思われる 使い方について書くことにする。

まず、前の記事では、Orderingの使い方を通して、型クラスの単純な使い方について説明したのであった。単純なオブジェクトの場合はそれでもいいが、より複雑なオブジェクトをOrderingを使ってソートしたい場合、前回の記事のようなやり方だけでは難しいことがある。

一例として、(A, B)というタプル型、つまり、A型の要素とB型の要素からなるペアを比較して、ソートしたいという要求を考える(実際には、標準ライブラリでタプル型の比較が提供されてしまっているので、新しい型MTuple[A, B]について考える)。このとき、タプル型同士を比較するOrderingを提供する段階では、ABも何が来るか全く不明なので、一見、そのままではOrderingを提供できないように見える。しかし、実は型クラスでは一般にそれができるのである、ということを示そう。

最初の、しかし、うまく動かないバージョンを以下に示す:

case class MTuple[A, B](_1: A, _2: B)
implicit def tupleOrdering[A, B]: Ordering[MTuple[A, B]] = new Ordering[MTuple(A, B)] {
  override def compare(x: MTuple[A, B], y: MTuple[A, B]): Int = {
    ???
  }
}

前の記事ではimplicit objectであったのが、implicit defになっているが、これはobjectジェネリックになれないため、仕方なく(?)そうなっているだけあり、本題とは関係ないので気にしないで欲しい。

???の部分を埋めて実装を完成させたいのだが、問題は、ABが何者であるかわからないため、比較を実装するのが無理である点だ。このcompareを実装するためには、Ordering[A]Ordering[B]の二つのインスタンスが必要で、しかも、今、私はオブジェクトを暗黙に渡して欲しいのだから、これらを明示的に与えるのは論外だ。では、どうすればいいのか?

回答を先に示す:

import scala.math.Ordering
case class MTuple[A, B](_1: A, _2: B)
implicit def tupleOrdering[A, B](implicit a: Ordering[A], b: Ordering[B]): Ordering[MTuple[A, B]] = new Ordering[MTuple[A, B]] {
  override def compare(x: MTuple[A, B], y: MTuple[A, B]): Int = {
    val MTuple(x1, x2) = x
    val MTuple(y1, y2) = y
    val r1 = a.compare(x1, y1)
    if(r1 < 0) {
      -1
    } else if(r1 > 0){
      1
    } else {
      b.compare(x2, y2)
    }
  }
}

利用例は以下のようになる:

List(MTuple(1, 2), MTuple(4, 3)).sorted  // => List(MTuple(1, 2), MTuple(4, 3))
List(MTuple(4, 3), MTuple(1, 2)).sorted // => List(MTuple(1, 2), MTuple(4, 3))
List(MTuple(new Object, new Object), MTuple(new Object, new Object)).sorted // => error: No implicit Ordering defined for MTuple[Object,Object].

最後の例は、MTupleの構成要素であるObjectに対して、Orderingが定義されていないので、したがって、MTuple[Object, Object]インスタンスも見つけられないということを意味している。

ある型クラスが別の型クラスに依存しているようなパターンといえる。このようなパターンは、単純なStrategyを明示的に渡すパターンではそのまま模倣しづらい(あえていうと、Strategyがネストしたオブジェクトを渡せば可能だがとても面倒だ)。

このような、型クラスが別の型クラスに依存するようなパターンは、おそらく、たとえば、open classのようなものでは実現が難しいし、通常のインタフェースでも実現が難しいであろう。

このとき、Ordering[MTuple[A, B]]の結果が、ユーザが実装をみないでも予想できるくらい十分にわかりやすいものになっているようにする必要がある点には注意されたい。

今回は、タプルの第一要素を先に比較して、それで順序が決まる場合はそれに従った順序に、それで決まらない場合(第一要素が同じである場合)、第二要素の比較を行うという形の辞書順比較なので、十分わかりやすいと言えるだろう。

上記の実装の意味については、また別の記事で解説したい。今回は、型クラスの本当のパワー(というとHaskellerの方に怒られそうではあるが、型クラス的なものでなくては実装が面倒なパターン)を見せるのが主な目的であるので。

型クラスを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度読んでわかるあたり、割と良い作品である証だと思います。裏がわかったら冷めるとかいうタイプではなく、裏がわかったあとで、最初から読み直してみると全く違った風に読めます。

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

Scalaでimplicits呼ぶなキャンペーン

  • はじめのはじめに

    • implicitsは可読性が…という人が居たら、この記事へのリンクを教えてあげていただければと思います
  • TL;DR

    • Scalaには俗に「implicits」と呼ばれる機能がある
    • 実際にはそれらは3つの機能をまとめて指す用語である
    • それぞれの機能は全く異なる
    • 暗黙の型変換は現代では無視してよし
    • 拡張メソッドは、既存の言語でそれを持つのと同じ程度の簡単さである
    • 型クラスとしての用法は、型クラスがある言語を知らないとやや難しいが、積極的に使う価値がある機能である
    • 「implicits」は異なる3つの機能を一つにまとめた呼び方に過ぎず混乱を招くので、使うのをやめよう(個々の機能名で呼ぼう)。また、暗黙の型変換は無視して良いし、拡張メソッドはよく知られているので、型クラスとしての用法にフォーカスして良い
  • はじめに

Scalaには俗にimplicitsと呼ばれる機能(群)があります。(群)と書いたのは、implicitsというのは実は単機能ではなく複 数の機能を指すからなのですが、このことがScalaに関する理解を妨げ、また、Scalaのimplicitsを難しい、黒魔術である、という印象に貢献していると確信しています。

そこで、implicitsに見られる3つの機能群に対して、適切な名前をあたえることでむやみに恐れる必要がないことを示したいと思います。

  • 暗黙の型変換(implicit conversion)

皆さんがimplicitsと聞いたときに最も多く思い浮かべるであろう機能です。が、忘れてください。2017年どころか数年前から、もうこの機能は好ましいものとみなされなくなっており、最近では、Javaのコレクションとのimplicit conversionを提供する標準ライブラリJavaConversionsは非推奨になっています。本当に例外的な目的に限ってのみ使用しても良い機能です。再度いいますが忘れてください。覚える必要がありません。

  • 拡張メソッド(enrich my library)

()内はScalaで従来どう呼ばれていたか、という呼称ですが、一般的な呼称として拡張メソッドとして読んで問題ないでしょう。たとえば、整数nにn回繰り返すtimesメソッドを追加するのは

implicit class RichInt(self: Int) {
  def times(block: => Unit): Unit = {
    var n = 0
    while(n < self) {
      block
      n += 1
    }
  }
}

と書くことで実現できます。このメソッドは、Intをレシーバとする(self: Int)ので、

3.times {
  print("A")
} // => AAAを表示

のように呼び出すことができます。

実は、Scala 2.9以前はより冗長な記法を使う必要がありましたが、Scala 2.10(4年以上前にリリースされたバージョン)からはこの書き方で、Int型にtimesメソッドを生やすことができるようになりました。新しくScalaを始める人はこの書き方だけ覚えていれば(だいたい)大丈夫です。いちいちRichIntと名前を付けるのが煩わしいと感じるかもしれませんが、大した問題ではありません。余分な情報はRichIntの名前の部分のみですから。

拡張メソッドを持つ言語には、C#、Kotlin、Swift(条件によるインポートができないのでちょっと違いますが)があります。これらの言語で拡張メソッドの有害性をうたう人はあまり居ないので、この機能を問題にする必要はないでしょう(自分は同程度に「問題」だとは思うのですが、一般的にはそういう認識だろうと感じています)

  • 型クラス(type class pattern)

最後の一つである型クラスとしての使い方は、とても有用である一方で、若干難しいと感じられるものかと思います。 ここでは、その威力を示すにとどめて、詳細な解説は別のページを探してもらうことにしようと思います。Scalaのコレクションにsumメソッドというのがあります。

これは、コレクションの要素を全て「足し合わせて」その合計値を返す、というメソッドです。

たとえば、

List(1, 2, 3, 4, 5).sum // => 15

でも

List(1.5, 2.5, 3.5).sum // => 7.5

でも同様にメソッドが動作するという点です。sumメソッドが1バージョンしか定義されていないにも関わらず、です。また、自分で有理数型を定義した場合にも、(定義を一切変えずに)このsumメソッドを利用することができます。この機能が非常に強力であることが理解できたと思います。

型クラスとしての使い方は有用性が非常に高く、積極的に使う価値がある機能です。もちろん、きちんとしたドキュメンテーションは必要ですが、あまり恐れる必要はありません。

  • まとめ

implicitsと俗に言われる3つの機能について別個に解説しました。もう「implicits怖い」と言うのは止めましょう。そのような機能は「存在して」おらず、3つの機能が便宜上そう呼ばれているだけなのですから。さらに、暗黙の型変換は無視すべきですし、拡張メソッドは従来いくつもの言語に存在するのですから怖がるほどのものではありません。型クラスとしての使い方のみにフォーカスして理解すればそれで事足ります。これで、もう「implicits怖い」と言う必要はなくなりましたね?

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コミュニティに対してコミットできていない気がしますが、今年はもうちょっとコミットしたいところです。ではでは。