KotlinプログラマのためのScala入門(1)〜基本編〜

  • 11
    いいね
  • 0
    コメント

Scala関係のSlackチャットで、「KotlinプログラマのためのScala入門」を書いて欲しいという要望があり、そもそもそんな需要があるのかいなと思いつつも、せっかくリクエストがあったので書いてみることにします。オブジェクト指向と関数型の融合とかのごたく抜きで、KotlinとScalaの比較にしぼって書きます。

この投稿は、KotlinとScalaの共通項にフォーカスを当てることで、Scalaに対するなんとなく難しそうというイメージを払拭するのが目的なので「こんなに似ているならKotlinでいいじゃん」と思われる方も居るかもしれませんが、Scala独自のメリットについては、こちらをご覧ください。
KotlinプログラマのためのScala入門(2)〜発展編〜

変数定義

ScalaとKotlinで全く同じです。

val i = 1
println(i) // 1
i = i + 1 // コンパイルエラー
var j = 1
println(j) // 1
j = j + 1 // OK
println(j) // 2
val i = 1
println(i) // 1
i = i + 1 // コンパイルエラー
var j = 1
println(j) // 1
j = j + 1 // OK
println(j) // 2

間接的に違いが出る部分としては、Kotlinはスマートキャストがあるので、valで定義された変数をif文でチェックした場合に型が違ってくる等の点があります。

if

ifの文法は同じで、両方とも式ですが、スマートキャスト関連で違いが見られます。

val iLtj = if(i < j) "i < j" else "i >= j" // String
val x: Any = "FOO"
if(x is String) {
  // x はこの中ではString型
}
val iLtj = if(i < j) "i < j" else "i >= j" // String
val x: Any = "FOO"
x match {
  case x:String => // Scalaだと型に対するパターンマッチを使う
}

whenとパターンマッチ

Kotlinでは、複雑な条件に対する分岐を書くときは when を使うことが多いと思います。

when(v) {
  0 -> ...
  1 -> ...
}

Scalaではパターンマッチを使います。

v match {
  case 0 => ...
  case 1 => ...
}

クラス定義

Scalaのクラス定義の文法をKotlinが継承したという経緯があるので、ほとんど同じように書けます。まずはKotlin版です。

class Person(val name: String, val age: Int)

次にScala版です。

class Person(val name: String, val age: Int)

全く同じです。

data classとcase class

Kotlinでは、値オブジェクトを定義するのにdata classを使います。

data class Person(val name: String, val age: Int)

Scalaでは、(パターンマッチに利用できることを除いて)ほぼおなじのcase classがあります。これも、KotlinがScalaから継承したものです(キーワードを除いて)

case class Person(name: String, age: Int)

若干違う点としては、Scalaではcase classのプライマリコンストラクタ引数は val をつけなくても自動的に val になる点でしょうか。

どちらの場合でも、

  • toString()
  • hashCode()
  • equals()

が適切に実装されます。

代数的データ型とパターンマッチ

Scalaでは代数的データ型を表現するのには、sealed (class/trait) と case classの組を使います(代数的データ型の説明は割愛)。

sealed abstract class ParseResult[+T]
case class ParseSuccess[+T](value: T, next: String) extends ParseResult[T]
case class ParseFailure(message: String) extends ParseResult[Nothing]
val result: ParseResult[String] = ...
result match { // 通常、case classの中身をパターンマッチであらかじめ分解する
  case ParseSuccess(v, n) =>
  case ParseFailure(m) =>
}

これに対応するKotlinのコードは次のようになります(Kotlin 1.1以降)。

sealed class ParseResult<out T> {
    data class ParseSuccess<out T>(val value: T, val next: String): ParseResult<T>()
    data class ParseFailure(val message: String): ParseResult<Nothing>()
}
val result: ParseResult<String> = ...
when(result) {
  is ParseResult.ParseSuccess -> // result: ParseSuccess
  is ParseResult.ParseFailure -> // result: ParseFailure
}
  • Kotlinの場合は、代数的データ型のための名前空間を作ることを強制されるのに、Scalaはそうでない
  • Kotlinはパターンマッチがないので、data classの中身を手動で分解する必要がある

あたりが主な違いでしょうか。

トレイトとインターフェース

Scalaには多重継承できる構成物としてトレイトがあり、Kotlinにはインターフェースがあります。どちらも実装を持てるという点で共通していますが、トレイトは初期化済みメンバも持てるという点が違いです。他にはクラス線形化とか細々とした違いがあるのですが、割愛します。

interface Foo {
  fun foo() {
    println("Foo")
  }
}
trait Foo {
  def foo(): Unit = {
    println("Foo")
  }
}

null安全とOption

自分はnull安全というのはマーケティング用語のようであまり好きではないのですが、便宜的にそう呼びます。Kotlinではnullが入り得る型とそうでない型が型システム上で区別されています。

val s: String? = null
val s: String = "FOO"

Scalaでは、型システム上nullを特別扱いしませんが、Option型を使って対処します。

val s: Option[String] = None
val s: String = "FOO"

型階層

これに関しても、KotlinとScalaは非常に類似しています(nullableに関するシステムを入れるとちょっとややこしくなるので割愛)。

  • 全ての型のスーパータイプ型は Any
    • ? を考慮すると、正確には Any?
  • 全ての型のサブタイプは Nothing
  • プリミティブ型相当の型名は以下(両者とも同じ名前)
    • Byte
    • Short
    • Int
    • Char
    • Float
    • Double
    • Boolean
    • Unit

Javaとの相互運用性

どちらの言語も、Javaのクラスを継承したり、Javaのメソッドを呼び出したりといった点において、ほとんど差はありません。たとえば、 Integer.parseInt を使うサンプルでは

val v = Integer.parseInt("100")
val v = Integer.parseInt("100")

となります。Javaから(Kotlin/Scala)を呼び出す際には、若干Kotlinの方が書きやすくなっています。

スコープ関数

Kotlinには、特にnullを扱うための便利なスコープ関数がいくつもありますが、同等物はScalaにはありません。Scalaの場合、Optionにあるメソッドがそれに該当するといえるかもしれません。

ラムダ式と無名関数

Kotlinでは、式中に {} と書いた場合、(いくつかの例外を除いて)ラムダ式とみなされます。

val proc: () -> Int = { 1 }

Scalaでは、 () => expr{() => expr ...} を使います。

val proc: () => Int = {() => 1}

また、Kotlinでは、引数が1個のラムダ式の場合、それを特別なキーワード it で参照できますが、該当する機能はScalaにはありません。明示的に名前をつけてくださいということになります。

val power: (Int) -> Int = { it * it }
val power: Int => Int = {(it) => it * it}

ちなみに、どちらも、関数の型を実装するアプローチはやはり全く同じで、引数の数の22制限まで同じです。

例外

同じです。

class MyException(override val message: String): Exception(message)
throw MyException("Hoge")
class MyException(val message: String) extends Exception(message)
throw new MyException("Hoge")

ちなみに、どちらも、未実装のメソッドを表すためのメソッドを持っています。

fun foo(): Nothing = TODO()
def foo() = ???

記号の選択に、それぞれの「らしさ」が出ていますね…と言いたい所ですが、ScalaでもTODO派と???派があって、最終的に???派が勝ったというだけの話なので、あんまり思想は関係なかったりします。

コレクション

Kotlinは java.util.* のコレクションを再利用するという道を選びましたが、Scalaは独自のコレクションライブラリ層を構築するという道を選びました。

val list = listOf(1, 2, 3)
list.filter{ it > 2 } // [3]
val list = List(1, 2, 3)
list.filter{it => it > 2} // List(3)

Kotlinアプローチは、ランタイムが軽量になるというメリットがありますが、Scalaアプローチは、不変性を型で保証できるというメリットがあります。

ビルドシステム

Kotlinではgradleを使うのが一般的ですが、Scalaではsbtを使うのが一般的です(ただし、gradleでも使えます)。両者のアプローチのどちらがいいかはわかりませんが、

  • 最小限の構成を書くのはsbtの方が楽
  • 複雑な構成を書くのはgradleの方が楽

という印象を持っています。

その他

  • レシーバ付き関数型(Kotlin)
  • 暗黙の引数(Scala)
  • 変性(Kotlin/Scala)
  • 高階多相(Scala)
  • , etc.

まとめ

タイトルに反して、KotlinとScalaの比較みたいになってしまいましたが、超高速で書いたのでご容赦を。また、両言語のサンプルコードは、自分が記憶している文法から、リファレンスを何もみないで書いたものなので、凡ミスがいくつもあると思います。その点はコメント欄で指摘していただければと思います。

この記事で何が言いたかったかというと、Scalaはよくわからないけど、Kotlinはわかりやすいように感じた方に、実は両者の学習コストというのはそんなに大きな差がないという点です。

基本的な部分で両者は共通している面が多いので、どちらかの言語を習得している人がもう片方の言語を習得するのにさほどのコストはかかりません(特に、文法の類似性、というか同一性は大きな助けになるでしょう)。現時点で、サーバーサイドではScalaの方がよく使われているという現状もありますし、Scalaを食わず嫌いしていた方も、これを機会にScalaに挑戦してみるのも悪くないのではないかなと思います。