2015年1月21日
Java 8とScala ‐アプローチの違いと相互イノベーション
本記事は、原著者の許諾のもとに翻訳・掲載しております。
ScalaとJava 8に関する プレゼンテーション が、他の似た内容のものよりも多くリツイートされ、大変うれしく思います。だから、こうして皆さんにブログでも書いてお伝えすることにしました。ScalaとJavaとの違いと、それぞれの重要性についてお話しします。両者は相互にイノベーションしています。言語間でお互いに取り入れています。では、Javaが使える場合であっても、Scalaを学ぶ必要があるのでしょうか? もちろんです。より多くの言語を知れば知るほど、あなたはさらにプロフェッショナルになっていきます。
もし、ScalaエンジニアにScalaとJavaとの基本的な違いについて尋ねたとしても、おそらくその人はラムダ関数とトレイトに関する違いを全て言うことはないでしょう。代わりに次のような例を出すはずです。
Java
public class Person
{
private String firstName;
private String lastName;
String getFirstName() { return firstName; }
void setFirstName(String firstName) { this.firstName = firstName; }
String getLastName() { return lastName; }
void setLastName(String lastName) { this.lastName = lastName; }
int hashCode() ....
boolean equals(Object o) { .... }
}
Scala
case class Person(firstName:String, lastName:String)
このように、Javaでは20行あるコードがScalaで書けば1行で済みます。また、簡潔さに欠けるのは、言語としてのJavaに限ったことではなく、Java開発者の世界で形成された文化にも言えることです。実際、上記のコードは次のように書くこともできます。
public class Person extends DTOBase
{
public String firstName;
public String lastName;
}
DTO基底クラスの hashCode と equals はリフレクションにより再定義されます。コーディングが簡潔であることを期待されているのはScalaが証明済みなので、getterとsetterを使わずにフィールドを宣言しても、責められることはありません。つまりJava開発の作法も簡潔さを目指す方向に進化しているのです。
Java 8は、関数型プログラミングスタイルが実現しやすいようにイノベーションされました。このイノベーションはScalaの構造に似た形を踏襲していることが一目で分かります。例えば次のような部分です。
- ラムダ式(無名関数)
- インターフェースのdefaultメソッド(Scalaで言えばトレイト)
- Streamのコレクション操作
詳細を見ていきましょう。
ラムダ式
Java
list.sort((x,y)-> {
int cmp = x.lastName.compareTo(y.lastName);
return cmp!=0 ? cmp : x.firstName.compareTo(y.firstName)
}
Scala
list.sort((x,y) => {
val cmp = x.lastName.compareTo(y.lastName)
if (cmp!=0) cmp else x.firstName.compareTo(y.lastName)
}
コードが酷似しているのが分かりますね。ところが、次はどうでしょうか。
Scala
var (maxFirstLen, maxSecondLen) = (0,0)
list.foreach{
x => maxFirstLen = max(maxFirstLen, x.firstName.length)
maxSecondLen = max(maxSecondLen, x.secondName.lenght)
}
Java
[? ](ラムダ式が呼び出された元の内容を変更することはできません)。
Javaのラムダ式は、コンテキストのfinalオブジェクトにのみアクセスを持つ匿名クラスの糖衣構文です。しかし、Scalaの場合は、コンテキストに完全にアクセスできる純粋なクロージャなのです。
インターフェースのdefaultメソッド
JavaがScalaから取り入れたもう1つの特徴は、インターフェースに記述するdefaultメソッドです。Scalaのトレイトに多少なりとも似ています。
Java
interface AsyncInput<T>
{
void onReceive(Acceptor<T> acceptor)
default void read(): Future<T> {
final CompletableFuture<T> promise = new CompletableFuture<>();
onReceive( x -> promise.complete(x) );
return promise;
}
}
Scala
trait AsyncInput[T]
{
def onReceive(acceptor: T=>()): Unit
def read: Future[T] = {
Promise p = Promise[T]()
onReceive(p.complete(_))
p.future
}
}
一見すると、2つはソックリですね。しかし次はどうでしょうか。
Scala
trait LoggedAsyncInput[T]
{
override def onReceive(acceptor: T => ()) =
super.onReceive(x => { println(s“received:${x}”)
acceptor(x) })
}
Java
[? ](Javaには相当する機能がありません。アスペクト指向アプローチのようなものがこれに相当します)。
別の例を見てみましょう(重要度は下がります)。
Scala
trait MyToString
{
override def toString = s”[${super.toString}]”
}
Java
[? ](インターフェースのオブジェクトメソッドをオーバーロードすることはできません)。
トレイト と default メソッドの構造はかなり違うことが分かります。Javaではコール・ディスパッチの仕様です。一方Scalaの場合、トレイトはfinalクラスが線形化されるので、より一般的な構造をしています。
Streamの操作とコレクション
Java 8の3番目のイノベーションはコレクションへのStreamインターフェースです。設計上はScalaの標準ライブラリに似ています。
Java
peoples.stream().filter( x -> x.firstName.equals(”Jon”)).collect(Collectors.toList())
Scala
peoples.filter(_.firstName == “Jon”)
両者は非常に良く似ていますが、Javaでは最初にコレクションからStreamインターフェースを生成し、その結果を構成するインターフェースに変換する必要があります。インターフェースをカプセル化するためです。
Javaに関数型ではないコレクションAPIが用意されているのであれば、そこに関数型のインターフェースを追加するのは適切ではない、ということになります(API設計とその修正、利用、理解を簡素化する話の場合)。これは、徐々に進化する過程での苦肉の策ということになります。
比較を続けましょう。
Java
persons.parallelStream().filter( x -> x.person==”Jon”).collect(Collectors.toList())
Scala
persons.par.filter(_.person==”Jon”)
解決策はよく似ています。Javaでは”parallel”なStreamを生成し、Scalaでは”parallel”なコレクションを作成します。
SQLデータベースへのアクセスは次のようになります。
Scala
db.persons.filter(_.firstName === “Jon”).toList
Javaのエコシステムにも 類似のもの があります。以下のように書けます。
dbStream(em,Person.class).filter(x -> x.firstName.equals(“Jon”)).toList
データベーステーブルのコレクションを実装する方法を、両方の言語で見るのは非常に面白いです。
Scalaではデータを操作するための型があります。型に関して大まかに言うと、以下のようになります。
persons は TableQuery[PersonTable] 型 である。
PersonTable \<: Table[Person]の関係で、 firstName と lastName のメソッドを持つ構造、ということになります。
firstName === lastName は===を使った2項演算です(Scalaでは2項演算を定義できます)。これは、Column[X] * Column[Y] => SqlExpr[Boolean]と似ています。
filter SqlExpr[Boolean] Query[T]
ここには、
filter: SqlExpr[Boolean] => Query[T]
というメソッドと、SQLを生成するメソッドがあります。従って、私たちはTable[Person]に対する式を表すことができ、これがPersonの表現になります。
非常にシンプルで、平凡ですらあります。
ではこの機能がJinqでどのように実装されるか見てみましょう。
dbStream(em,Person.class).filter(x -> x.firstName.equals(“Jon”)).toList
x の型はここでは Person であり、 x.firstName は String です。 filter メソッドが Person -> Boolean 関数をパラメータとして受け取ります。ではここからどのようにSQLを生成するのでしょうか?
filter はバイトコードを解析します。命令を”シンボリックに”実行するバイトコード解析器のようなものがあります。実行結果はgetterと関数呼び出しのルートになり、SQLを生成するのに使われます。
これは実に良いアイデアではあります。しかし、この全てはプログラム実行中に動的に実行されますから、かなり長い時間が必要です。仮に filter メソッドの内部で修正リストにはない関数を使おうとしたら(しかもSQLをどう書けばいいか分からない)、私たちは実行中にしか、それに気付けないでしょう。
従って、Scalaのコードは多かれ少なかれ平凡なものになります。ですが一方で、とても洗練されたJavaの技術を利用しているとも言えます。
これが、ScalaとJavaが、お互いを取り入れあっているということなのです。このように、≪複数バージョンで同じ機能が使える≫Javaは、Scalaとは全く異なるものなのです。
ScalaがJavaから取り入れたもの
ではJava 8とScalaがお互いを取り入れあっていることについて考えましょう。イノベーションのプロセスは双方向です。Java 8のイノベーションは、Scalaから取り入れられました。バージョン2.11では、私たちはコンパイル・オプションによるヘルプで、実行が簡単になりました。2.12ではこういったヘルプはデフォルトになっています。SAMタイプへの変換について考えてみましょう。
以下にコードの一部を示します。
Java
interface AsyncInputOutput<T>
{
void onReceive(Acceptor<T> acceptor)
void onSend(Generator<T> generator)
}
Scala
trait AsyncInputOutput[T]
{
def onReceive(f: T => Unit): Unit
def onSend(f: Unit => T): Unit
}
このように、Javaにおける型とメソッドのパラメータは、 値を受けています し、 値を生成 もしています。バイトコードのレベルではクラス間のやり取りとして表現されます。Scalaにおいては、T=>UnitとUnit=>Tの関数であり、Function1.classとして表現されます。
SAM(Single Abstract Method)型は、抽象メソッドを1つしか持たないクラスまたはインターフェースです。Javaでは、メソッドがSAM型をパラメータとして受け取ると、関数を渡すことが可能です。この状況はバージョン2.11以前のScalaと異なります。関数は、Function[A,B]のサブクラスなのです。
一見すると、変化はそれほど激しくはありません。ただしそれは、オブジェクト指向で関数型APIを記述できるという事実を除いた場合です。実際問題として、この機能には大変重要な妥当性があります。それは、時間についてクリティカルな箇所で関数型インターフェースを使うと分かります。なぜでしょうか? JITインタプリタ上にてバイトコードで実行する場合、その効率性は、強引にインライン化できるかどうかによって変わるからです。
しかしもし関数型インターフェースで扱うなら、パラメータのクラスは、パラメータを1つ取る関数Function1や、パラメータを2つ取る関数Function2などに似ているでしょう。インライン化するにはとても難しいのです。これが、問題が見えていなかった理由です。関数型インターフェースは、実行時間にクリティカルで低水準な箇所のコードには適していません。ですから、そもそもJITではインライン化することができません。SAMではローカルのSAM型を通して書き直すことが可能で、この場合はコンパイラがインライン化してくれます。こうして、問題は解消されるでしょう。
しかしそれでも既存のコードを変更する必要があるでしょう。SAMを通して(コレクションのインターフェースといった)幾つかの事項を書き直すことはできます。その場合は新しいインターフェースと古いインターフェースを結合できますので、全て一緒に確認することになります。
Javaにも同じ問題があります。つまり、コレクションのインターフェースです。これによって、どのように発展してきたかが分かります。ある意味においては、これこそがJavaを改良させてきたのです。完璧ではありませんが、以前より改良されました。一方でScalaも、完璧には達していませんが改良されています。しかしどちらの言語にも2つの≪バグ≫があります。それは、ゆっくりと適合を進めているために引き起こされています。幾つか区切られた期間の中には、”完璧な”インターフェースを提供しうる他言語のために、空白期間が設けられています。それこそが、発展していくために必要なのです。
Javaではできないが、Scalaでは構造を2グループに分けられる
-
バージョン9、10、11、12のJavaで使われるだろうグループ(もし新バージョンがリリースされても、誰にとってもJavaが面白い言語であり続けたなら)。Fortran90がオブジェクト指向となったように、発展する道理となるでしょう。
-
JavaとScalaの思想の違いを表すグループ。
1つ目のグループに関してですが、caseクラスと自動の型インターフェースに名前を付けることが可能です。他の全てのものは2つ目のグループとなります。
最初に、次のコードを使います。
case class Person(firstName: String, lastName: String)
なぜcaseクラスを case と名付けるのでしょう? それは、matchやcaseといった演算子を使えるからです。
p match {
case Person(“Jon”,”Galt” ) => “Hi, who are you ?”
case Person(firstName, lastName) => s”Hi, ${firstName}, ${lastName}”
case _ => “You are not person”
}
最初のcaseはJon Galtという名前に対する処理です。2つ目のcaseはPersonにそれ以外の全ての人名を渡された場合の処理です。この時、渡されたfirstNameとlastNameを返して、名前を使って挨拶しています。これはMLスタイルと呼ばれるパターンマッチングです。MLスタイルと呼ばれる理由は、これが1973年に発明されたML言語で提案された最初の構造だからです。
今日では、ScalaやKotlin、Ceylon、Apple Swiftといったほとんどの”新しい”言語が、これをサポートしています。
Scalaの特性
それでは、Scalaの主な特徴は何でしょうか? Javaでは実現できないけれどもScalaで実現できるのは、どんなことでしょうか? それは内部DSL[Domain Specific Language(ドメイン固有言語)]の構築能力です。つまり、 Scalaは非常に体系的なので、あらゆるオブジェクトエリアにおいて、厳密な型モデルを構築し、言語構造の中でそれを表現することが可能なのです。
このような構造は静的に型付けられた環境の上に構築されています。そのような構造を構築できる基本的特徴とは何でしょうか?
- 柔軟な構文、糖衣構文
- 名前でパラメータを渡す構文
- マクロ
まず柔軟な構文から見ていきましょう。実際のところ、どのような意味があるのでしょうか。
1. メソッドのネーミング方法に制限がないこと
def +++(x:Int, y:Int) = x*x*y*y
2. 中置メソッドが1つのパラメータでメソッドを呼び出すこと
1 to 100 == 1.to(100)
3. 丸括弧と波括弧の唯一の違いは、波括弧では複数の式を括弧内に持てるということであるが、パラメータが1つの場合は丸括弧と波括弧との間には違いがないこと
future(1) == future{1}
4. 引数のリストを持ちながら関数を定義できること
def until(cond: =>Boolean)(body: => Unit) : Unit
5. コードブロックを関数の引数として渡せるため、対応する引数が変化した時に(引数を”名前”で渡し)、毎回関数が呼び出されること
def until(cond: =>Boolean)(body: => Unit):Unit =
{ body; while(!cond) { body } }
until(x==10)(x += 1)
それでは、Do-until文のDSLを書いてみましょう。
object Do
{
def apply(body: => Unit) = new DoBody(body)
}
class DoBody(body: => Unit)
{
def until(cond: =>Unit): Unit =
{ body
while(!cond)
body
}
}
そして、以下のようなコードを書くことができます。
Do {
x += 1
} until ( x != 10 )
DSLを構築する機能があるため、専用の関数の中には特別な構文を持つものもあります。
例えば、以下のような式があるとしましょう。
for(x <- collection){ doSomething }.
これはメソッドを呼び出す単なる構文ですね。
collection.foreach(x => doSomething)
そこで、ある関数([X] => Unit)を実現するため foreach メソッドを使って独自のクラスを書く場合、独自のタイプとして、 foreach 構文を使えるのです。
同じことが for-yield 文( map で使う)の構造と、ループでの入れ子になった繰り返し処理( flatMap )の条件演算子にも言えます。
つまり、こういうことです。
for(x <- fun1 if (x.isGood);
y <- fun2(x) ) yield z(x,y)
上記のコードを別の構文で書くと以下のようになります。
fun1.withFilter(_.isGood).flatMap(x => fun2.map(y => z(x,y)))
ちなみに、 Scala-virtualized と呼ばれているScalaを拡張したものがあります。これは、独立したプロジェクトで、残念ながらScalaの標準にはならないと思われます。全ての統語構造は仮想化されています。つまり、if-uやmatch式などのことです。完全に異なる意味を挿入することもできます。GCPUのためのコード生成、機械学習のための専門言語、JavaScriptへの変換などは、アプリケーションの例として挙げられます。
同時に、現在のエコシステムにはJavaScriptへのプログラムコンパイルが存在します。その機能性は、JavaScriptを生成することができるScalaのコンパイラ、scala.jsに移植されました。このコンパイラはいつでも自由に使えます。
更にScalaの便利な機能として マクロ があります。マクロはコンパイル時にプログラムのコードを変換するものです。簡単な例を見てみましょう。
object Log
{
def apply(msg: String): Unit = macro applyImpl
def applyImpl(c: Context)(msg: c.Expr[String]):c.Expr[Unit] =
{
import c.universe._
val tree = q"""if (Log.enabled) {
Log.log(${msg})
}
"""
c.Expr[Unit](tree)
}
}
Log(message)の式は、以下のように表記することができます。
if (Log.enabled) {
Log.log(message)
}
なぜ、これが便利なのでしょうか?
1つ目に、マクロを使うことで、いわゆる”ボイラープレート”コードを生成できます。これは一目瞭然ですが、コードを生成できるとは言っても、何らかの形で記述しなければいけません。そこでは、xml/jsonコンバータやcaseクラスを名付けることができます。Javaのボイラープレートでは、リフレクションを利用してコードを≪短くする≫こともできます。しかし、これにより実行スピードに対して、ところどころに致命的な制限がかかってしまうのです。つまりリフレクションは自由に利用できるわけではありません。
2つ目に、マクロを使うことによって、プログラムにおいて単に関数を受け渡すだけではなく、更に重要な変更を加えることができます。実際に、構造を独自に実装、つまりグローバルな更新ができるのです。
例として、asyncインターフェースに名前を付けてみましょう。これは、C#のインターフェースをコピーしたものです(すなわちasyncブロックの途中です)。
async {
val x = future{ long-running-code-1}
val y = future{ long-running-code-1}
val z = await(x)+await(y)
}
このコードブロックをそのまま読むと、 x と y が計算を実行し、その計算が完了するのを z が待つということが読み取れます。要するに、全てのコンテキストスイッチはノンブロッキングにするという方法で async のコードを書き換えるのです。
asyncやawait APIがマクロのライブラリとして作られていることは興味深いですね。つまり、Scalaでは単にライブラリを書くことができる一方で、C#では新たなコンパイラをリリースする必要があるということです。
他の例としては、 jscala があります。jscalaはScalaコードのサブセットをJavaScriptに変換するマクロです。ですので、もしフロントエンドの開発を行いたいと考えていて、JavaScriptに切り替えたくない場合でも、Scalaを使えば同じことができて、マクロが必要なことをやってくれるわけです。
まとめ
要約すると、既に存在している内容のオペレーション領域で、その抽象レベルがクラスとオブジェクトである場合において、JavaとScalaを比べることは多かれ少なかれ合理的であると言えます。抽象レベルを上げ、何か新しいことを説明する必要がある場合は、Scalaにおける内部のDSLを考えることができます。Javaに関しては、外部DSLの構築か、アスペクト指向プログラミングのような解決策の適用を試すことができるのです。
どんな状況においても絶対的に良いアプローチというものはありません。Javaでは、言語の境界を離れ、基板を構成すべき時があるということを理解しておきましょう。Scalaでは、”言語内で”基板を構成することができます。
そうは言ってもScalaには、たくさんの内部的な問題もあります。機能的にアンバランスな時もあるのですが、これについて話すと話が長くなりそうです。たくさんの実験的な解決策があり、そのような機能が開発されるのを見られるのはすばらしいことですね。現在、私たちは新たな開発段階に入り、現在の構成における問題点だけではなく、機能を構築するという局面を見ることができるように思われます。単にJavaにはこのような局面はないということです。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa