Scalaで関数脳を身につけた俺が仕方なくKotlinに手を出す
はじめに
仕事のメイン言語は長らくJavaだが、Scalaを趣味プロダクトのメイン言語としてずっと使ってきた。 Haskellも齧った自分は関数型脳をそれなりに身に着けているという自負がある。モナドも道具として使えている。
時に、西暦2018年。新規プロジェクトの機運高まり、言語選定にてScalaを推すも、思いもよらなかった新興勢力が立ちはだかっていた。 Scala界隈からすると後発の新興勢力であるKotlinだ。職場ではこれを推す声が大きく、逆にScala推しは自分だけであった。 このような状況から、政治的にもScalaは劣勢で、“Java or Kotlin”という対立軸が形成されつつあった。
こんな状況なので、Scalaで関数脳を身につけた俺が仕方なくKotlinに手を出すことになった。
やること
- ScalaとKotlinの歴史的経緯の比較
- ScalaとKotlinの社会状況の比較
- ScalaとKotlinにおける主要なライブラリの使用感の比較
やらないこと
- ScalaとKotlinの文法比較
対象読者
- Scalaをある程度使いこなしている人
- Kotlinに仕方なく手を出さざるを得ない人
ScalaとKotlinの歴史の振り返り
2018年時点でのScalaとKotlinの歴史を振り返りたい。
Scalaの歴史
関数型言語とオブジェクト指向言語の統合を目的とした言語。スイス生まれ。 2001年に Martin Odersky により設計され、2004年に最初のJava向け実装が公開された。 ScalaはJavaの拡張ではないが、完全に相互運用可能。ScalaはJavaのバイトコードに変換され、プログラムの効率はJavaのものと同等。1
Kotlinの歴史
産業用を志向した汎用言語。ロシア生まれ。 JetBrains社により作成され、2011年に公開された。 Kotlinの目標は、Javaより安全で簡潔でNull安全であること。もう一つの目標は、「最も成熟した競合」であるScalaよりも簡単であること。2
Scala勢力とKotlin勢力の社会状況とお互いの視点
2018年時点でのScala勢力とKotlin勢力の各々の視点を分析したい。 あくまでも自身がScala勢力であるため、この視点に立った分析となることをお許し頂きたい。
Scala勢力
Scala勢力はKotlinをどのように見ているか。 KotlinはScalaと大変似ているため、Scala勢力からすると何故新たに言語を作ったのか理解できない。 Scalaと似ている割に、Kotlinには機能不足やコンセプトの不徹底を感じており、存在意義を疑っている。 機能不足の点では、Kotlinに対応したJavaライブラリや、Pure Kotlinなライブラリが十分に出揃っておらず、実用的な印象を持てない。 コンセプトの不徹底の点では、Kotlinの目標 “Javaより安全で簡潔でNull安全であること” をScalaは既に満たしており、関数型言語を統合したScalaをある程度の正解と考えているため、Kotlinのコンセプトが不明瞭に見える。 KotlinがScalaに似ているからこそ、Kotlinが何をしたいのか分からないという状態になっている。
そのような評価とは裏腹に、KotlinはAndroidの開発言語として正式採用されるなど、出世イベントと共に社会の中で高く注目されている。 このような状況により、Scala勢力から見ると、Scalaが取るべきマーケットをKotlinが横取りしているように見えているのではないか。
その一方で、Scala勢力のなかでも一部の人々は、大規模なプロダクトをScalaで開発した上で、多人数開発でのScala採用に大きな課題を感じている。 そのような課題に対しては、解決案としてKotlinを良しとしている向きもある。
Kotlin勢力
Kotlin勢力はScalaをKotlinより過去のものと考えている。 これはScala勢力がJavaをKotlinより過去のものとして考えていることと同様で、単純に歴史だけ切り取ると順当といえる。 確かに各言語を純粋に登場時期だけで比較すると、Javaが1995年、Scalaが2004年、Kotlinが2011年なので、各々10年弱の開きがある。 Kotlin勢力には、そもそもScala勢力との対立意識は無い。
主要なライブラリ選定
何かプロダクトを作るに際して使用するライブラリは、Scalaでは主要なところはproduction-readyなものが一揃い存在する。 Kotlinではどうだろうか?ここでは各種ライブラリを対象にいくつか上げ、利用可能なライブラリの状況を整理する。
Scalaに関しては Awesome Scala (Star数6,182個)を参照し選択する。 Kotlinに関しては Awesome Kotlin (Star数5,412個)を参照し選択する。 各言語のStar数は2018年8月16日現在の数値。
ビルドツールはScalaではsbt、KotlinはGradleを使用する。
Webアプリケーションフレームワーク
Webアプリケーションを作るにあたり、サーバサイドではWebアプリケーションフレームワークを使用する。 フロントエンドの進化により、サーバサイドにてHTMLをレンダリングすることへの要求は低下している。 よって今回はフルスタックなものではなく、RESTful APIを構築するのに必要なだけのWebアプリケーションフレームワークを比較する。
作成するWebアプリケーションの仕様は http://localhost:8080/hello/pompom
へのアクセスに対し、テキストで Hello, pompom
を応答するものとして統一する。
Scalatra vs Ktor / Spring MVC
ScalaではScalatra、KotlinではKtorを選定した。 Ktorのバージョンが現時点で0.9.4であり、バージョン1以上のライブラリを求める場合には適さないため、代替案としてSpring MVCも確認する。
Scala: Scalatra
ScalaでRESTful APIを構築しようと考えるときの最初の候補は、Sinatraライクに実装できるScalatraだ。
sbtをインストールしていれば、 sbt new scalatra/scalatra.g8
でScalatraプロジェクトの雛形を作成できる。
サーブレットコンテナを利用するWebアプリケーションとなるため、以下のように記述した web.xml
がエントリポイントとなる。
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<listener>
<listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
</listener>
</web-app>
サーブレットのコンテキスト起動時に上記設定により ScalatraListener
がインスタンス化され、 contextInitialized
メソッドが呼ばれる。
ScalatraListener
はクラスパス内より LifeCycle
の子クラスを見つけ、発見した子クラスをインスタンス化し init
を呼ぶことでコンテキストを設定する。
src/main/scala/ScalatraBootstrap.scala
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) {
context.mount(new SampleScalatraServlet, "/*")
}
}
/*
のパスでマウントしている SampleScalatraServlet
は以下の通り。
src/main/scala/SampleScalatraServlet.scala
class SampleScalatraServlet extends ScalatraServlet {
get("/hello/:name") {
s"Hello, ${params("name")}"
}
}
ルーティングにおいては、 get
に渡す文字列内の :name
が params("name")
により取得となっているため、RESTful APIを構築するための記述としては大変シンプルな形となる。
Kotlin: Ktor
gradle init --type java-application
でJavaアプリケーションとして初期化。
以下のように build.gradle
を構成すれば、Kotlinアプリケーションとしてmain関数を実行できる。
build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.2.61'
}
apply plugin: 'application'
repositories {
mavenCentral()
jcenter()
maven { url "https://dl.bintray.com/kotlin/ktor" }
}
dependencies {
compile "io.ktor:ktor-server-netty:0.9.3"
}
mainClassName = 'AppKt'
実行するmain関数を持つ App.kt
ファイルの内容は次の通り。
App.kt
fun main(args: Array<String>) {
val server = embeddedServer(Netty, port = 8080) {
routing {
get("/hello/{name}") {
call.respondText("Hello, ${call.parameters["name"]}")
}
}
}
server.start(wait = true)
}
これだけでRESTful APIの構築が完了する。 サーブレットAPIを使用しないタイプのライブラリだからという理由もあるが、非常にシンプルになる。
参考: Quick Start - Quick Start - Ktor - http://ktor.io/quickstart/index.html
Kotlin: Spring MVC
JetBrains社が作っているKtorのバージョンが現時点で0.9.4であることからもわかるように、Kotlin界隈ではPure KotlinなWebアプリケーションフレームワークの成熟度は、現時点では不十分と言える。 このため、KotlinでWebサービスを作成しプロダクション投入する場合は、Javaにおいて十分に成熟しているSpringを利用するのが適切とも言える。
Spring Initializr でKotlinを指定しSpring MVCを選択してプロジェクトを生成する。
プロジェクトの雛形がダウンロードされ、 ./gradlew bootRun
によりSpring Bootが起動する状態の構築が完了する。
そこへ新規クラスを作成し、@RestController
を付与すれば、RESTful APIのエンドポイントを追加していくことができる。
src/main/kotlin/springsample/SpringSampleApplication.kt
@SpringBootApplication
class SpringSampleApplication
fun main(args: Array<String>) {
runApplication<SpringSampleApplication>(*args)
}
@RestController
class SpringSampleController {
@GetMapping("/hello/{name}")
fun hello(@PathVariable("name") name: String): String {
return "Hello, $name"
}
}
@SpringBootApplication
の付与されたクラスの存在するパッケージ以下が、Springによるコンポーネントスキャンの対象となる。
このため、Scalatra及びKtorのサンプルでは使用していなかったが、Springではパッケージを使用している。
JavaでSpring慣れしている人はKotlinでもほぼ同じ使用法でSpringのMVCやDIを使用可能なため、学習コスト低く使用開始できる。 Pure Kotlinのライブラリではないが、Spring側でもバージョン5から公式にKotlin対応を謳っているため、問題なく使用できるものと思われる。
テストフレームワーク
ScalaTest vs JUnit5
FizzBuzzを例として、次のJavaクラスをテスト対象とする。
src/main/java/FizzBuzz.kt
public class FizzBuzz {
public String exec(int i) {
if (i % (3 * 5) == 0) return "fizz buzz";
if (i % 3 == 0) return "fizz";
if (i % 5 == 0) return "buzz";
return String.valueOf(i);
}
}
Scala: ScalaTest
build.sbt
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test"
上記の通りScalaTestのライブラリを追加することで使用可能となる。
src/test/scala/FizzBuzzTest.scala
class FizzBuzzTest extends FlatSpec with Matchers {
val fizzBuzz = new FizzBuzz
"Any number" should "be expressed as number string" in {
fizzBuzz.exec(1) should be ("1")
fizzBuzz.exec(2) should be ("2")
}
"Any number divisible by three" should "be replaced by the word fizz" in {
fizzBuzz.exec(3) should be ("fizz")
fizzBuzz.exec(6) should be ("fizz")
}
"Any number divisible by five" should "be replaced by the word buzz" in {
fizzBuzz.exec(5) should be ("buzz")
fizzBuzz.exec(10) should be ("buzz")
}
"Any number divisible by three and five" should "be replaced by the fizz buzz" in {
fizzBuzz.exec(15) should be ("fizz buzz")
fizzBuzz.exec(30) should be ("fizz buzz")
}
}
Scalaの省略記法により、 X should be Y
のDSLが可能となっている。
sbt test
にて以下の出力を得る。
sbt testの実行結果
[info] FizzBuzzTest:
[info] Any number
[info] - should be expressed as number string
[info] Any number divisible by three
[info] - should be replaced by the word fizz
[info] Any number divisible by five
[info] - should be replaced by the word buzz
[info] Any number divisible by three and five
[info] - should be replaced by the fizz buzz
[info] Run completed in 395 milliseconds.
[info] Total number of tests run: 4
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 4, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
Kotlin: JUnit5
Pure KotlinなテストフレームワークとしてJetBrains社のSpekが存在する。 同社が付いているため心強いが、現在バージョン2に向け破壊的変更を予定しているため、現段階では選定対象とし辛い。 Javaの世界で磨かれたJUnit5がKotlinでも同様に使用できるため、これを選定した。
build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.2.61'
}
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile("org.junit.jupiter:junit-jupiter-api:5.2.0")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.2.0")
}
src/test/kotlin/FizzBuzzTest.kt
class FizzBuzzTest {
private val fizzBuzz = FizzBuzz()
@Test
fun anyNumberShouldBeExpressedAsNumberString() {
assertEquals("1", fizzBuzz.exec(1))
assertEquals("2", fizzBuzz.exec(2))
}
@Test
fun anyNumberDivisibleByThreeShouldBeReplacedByTheWordFizz() {
assertEquals("fizz", fizzBuzz.exec(3))
assertEquals("fizz", fizzBuzz.exec(6))
}
@Test
fun anyNumberDivisibleByFiveShouldBeReplacedByTheWordBuzz() {
assertEquals("buzz", fizzBuzz.exec(5))
assertEquals("buzz", fizzBuzz.exec(10))
}
@Test
fun anyNumberDivisibleByThreeAndFiveShouldBeReplacedByTheFizzBuzz() {
assertEquals("fizz buzz", fizzBuzz.exec(15))
assertEquals("fizz buzz", fizzBuzz.exec(30))
}
}
JavaでJUnit5を使用する場合と何ら変わらない使用法でテストを書くことができる。
ORM
Webサービスの永続化としてリレーショナルデータベースを使用する場合、ORMを使用するのが一般的だ。 ORMによりトランザクション制御・オブジェクトへのマッピング・コネクションプールの利用などを抽象化してくれる。
Slick3 vs Spring Data JPA
Scalaで最もGitHub上のスターを集めているのはSlick3である。 Lightbend社(2016年にTypesafeから社名変更)が主に提供しているOSSで、バージョン3から非同期かつノンブロッキングなインタフェースとなっている。
KotlinにはJetBrains社のExposedがあるが、これもバージョンが1に達していない。 そこで、Springと共に利用しやすいSpring Data JPAを使用する。
今回は、Slick3のサンプル3 に記載されているコーヒーショップのデータベースを作成する。
テーブルとしてはサプライヤーとして SUPPLIERS
テーブルと、サプライヤーへの参照を持つコーヒーとして COFFEES
を準備する。
Scala: Slick3
各RDBMS実装に対応する以下のようなトレイト JdbcProfile
の実装クラスが存在する。
- DB2Profile
- DerbyProfile
- H2Profile
- HsqldbProfile
- MySQLProfile
- OracleProfile
- PostgresProfile
- SQLiteProfile
- SQLServerProfile
これらの各々が、 JdbcProfile#api
を持っており、このAPIを用いてテーブル定義やクエリ定義が必要となる。
各RDBMSごとに別々のクエリ定義の実装を行いたくはないので、各 JdbcProfile
の実装クラスではなく、トレイト JdbcProfile
として取り扱う。
src/main/scala/ComponentAggregator.scala
class ComponentAggregator(val jdbcProfile: JdbcProfile)
extends CoffeesComponent
with SuppliersComponent {
import jdbcProfile.api._
def createSchema = {
(suppliers.schema ++ coffees.schema).create
}
}
後述する2つのトレイト CoffeesComponent
及び SuppliersComponent
を拡張し、コンストラクタ引数で JdbcProfile
を受け取っている。
src/main/scala/SuppliersComponent.scala
trait SuppliersComponent {
val jdbcProfile: JdbcProfile
import jdbcProfile.api._
val suppliers = TableQuery[Suppliers]
class Suppliers(tag: Tag)
extends Table[(Int, String, String, String, String, String)](tag, "SUPPLIERS") {
def id = column[Int]("SUP_ID", O.PrimaryKey) // This is the primary key column
def name = column[String]("SUP_NAME")
def street = column[String]("STREET")
def city = column[String]("CITY")
def state = column[String]("STATE")
def zip = column[String]("ZIP")
def * = (id, name, street, city, state, zip)
}
}
トレイト SuppliersComponent
の実装クラスで jdbcProfile
が与えられるものとし、 jdbcProfile#api
を用いてテーブル定義やクエリ定義を行う。
次の CoffeesComponent
も同様であるが、外部キーの指す SuppliersComponent
の依存のため、自分型アノテーションを用いる。
src/main/scala/CoffeesComponent.scala
trait CoffeesComponent {
self: SuppliersComponent =>
val jdbcProfile: JdbcProfile
import jdbcProfile.api._
val coffees = TableQuery[Coffees]
class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") {
def name = column[String]("COF_NAME", O.PrimaryKey)
def supID = column[Int]("SUP_ID")
def price = column[Double]("PRICE")
def sales = column[Int]("SALES")
def total = column[Int]("TOTAL")
def * = (name, supID, price, sales, total)
def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id)
}
}
self: SuppliersComponent =>
の部分が自分型アノテーションで、これにより SuppliersComponent
のメンバーを自身のものとして参照できる。
実際には CoffeesComponent
を拡張実装したクラスをインスタンス化するには SuppliersComponent
も実装する必要がある。
各テーブルのコンポーネントを集約した ComponentAggregator
に特定のRDBMSの JdbcProfile
を渡すことで、実際に使用可能な実装を得る。
次の例では ComponentAggregator(H2Profile)
とすることによりH2用として初期化している。
src/main/scala/Slick3Sample.scala
object Slick3Sample extends ComponentAggregator(H2Profile) {
def main(args: Array[String]): Unit = {
import scala.concurrent.ExecutionContext.Implicits.global
val db = Database.forConfig("h2mem")
val dbio = for {
_ <- createSchema
_ <- suppliers +=
(150, "The High Ground", "100 Coffee Lane", "Meadows", "CA", "93966")
_ <- coffees += ("Espresso", 150, 9.99, 0, 0)
message <- coffees.filter(_.name === "Espresso").map(_.price).result.headOption
} yield println(message)
Await.ready(db.run(dbio), Duration.Inf)
}
}
for文の波括弧内の各々の行にて、右辺がDBIOモナドを返している。
これらをまとめた dbio
をデータベース接続したインスタンス db
で走らせることで実際にアクションを実行している。
db.run(dbio)
はFuture[Unit]を返し、これは非同期実行のため、mainメソッドが終わってしまうと打ち切られてしまうため、 Await.ready
で終了を待っている。
第二引数は最大待ち時間で、 Duration.Inf
は無制限の時間を表す。
Kotlin: Spring Data JPA
Spring Data JPA のKotlinでの使用法はJavaとさほど差異はない。 ただし、Kotlinの言語仕様が次の2点でSpring及びJPAの利用に支障をきたすためGradleのプラグインにて対応が必要となる。4
クラス及びメソッド定義がデフォルトで final
この仕様のため、SpringのAOPがインスタンス生成時にクラス及びメソッドを拡張しようとし失敗する。
デフォルトで final
というだけなので、クラス及びメソッド定義に open
を付与すれば問題は回避できる。
具体的には @Configuration
及び @Service
を付与するクラスと、そのメソッドが対象となる。
Kotlinのこの言語仕様自体を無効化する手段がSpringより公式にMavenとGradleの両方のプラグインとして提供されている。 Gradleの場合はbuild.gradleに以下の通りプラグインを追加する。
build.gradle
buildscript {
dependencies {
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
}
}
apply plugin: "kotlin-allopen"
これにより “クラス及びメソッド定義がデフォルトで final
” という仕様により得られる効果は無効化されてしまうため、適用の是非は慎重に検討したいところだ。
データクラスがデフォルトコンストラクタを持たない
Kotlinの言語仕様には、データを保持するためのクラスとしてデータクラスが存在する。 Scalaでいうところのケースクラスに近い。 JPAのエンティティを定義するのに便利なこのデータクラスだが、JPAと仕様上のギャップが有る。 JPAにおいてエンティティはデフォルトコンストラクタを持つ必要があるが、データクラスはこれを持たない。
データクラスにデフォルトコンストラクタを付与する手段もSpringより公式にMavenとGradleの両方のプラグインとして提供されている。 Gradleの場合はbuild.gradleに以下の通りプラグインを追加する。
build.gradle
buildscript {
dependencies {
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
}
}
apply plugin: "kotlin-jpa"
こちらは先述の kotlin-allopen
プラグインが入っていれば不要となる。
アプリケーション実装
各テーブルに対応するリポジトリとエンティティ定義を行う。
src/main/kotlin/jpasample/Coffees.kt
@Repository
interface SupplierRepository : JpaRepository<Suppliers, Int>
@Entity
@Table(name = "SUPPLIERS")
data class Suppliers(
@Id
@Column(name = "SUP_ID", nullable = false)
var id: Int = 0,
@Column(name = "SUP_NAME", nullable = false)
var name: String = "",
@Column(name = "STREET", nullable = false)
var street: String = "",
@Column(name = "CITY", nullable = false)
var city: String = "",
@Column(name = "STATE", nullable = false)
var state: String = "",
@Column(name = "ZIP", nullable = false)
var zip: String = "",
@OneToMany
var coffees: MutableList<Coffees> = ArrayList()
)
SUPPLIERSと1対多の関係になるCOFFEESは空のListとして初期化すれば、リポジトリ経由で参照する際に取得してくれる。
src/main/kotlin/jpasample/Coffees.kt
@Repository
interface CoffeeRepository : JpaRepository<Coffees, String> {
fun findByName(name: String): Optional<Coffees>
}
@Entity
@Table(name = "COFFEES")
data class Coffees(
@Id
@Column(name = "COF_NAME", nullable = false)
var name: String = "",
@ManyToOne
@JoinColumn(name = "SUP_ID", nullable = false)
var supplier: Suppliers = Suppliers(),
@Column(name = "PRICE", nullable = false)
var price: Double = 0.0,
@Column(name = "SALES", nullable = false)
var sales: Int = 0,
@Column(name = "TOTAL", nullable = false)
var total: Int = 0
)
COFFEESと多対1の関係になるSUPPLIERSは @JoinColumn
で結合方法を指定すれば、リポジトリ経由で参照する際に取得してくれる。
src/main/kotlin/jpasample/JpaSample.kt
@SpringBootApplication
class JpaSample(
private val supplierRepository: SupplierRepository,
private val coffeeRepository: CoffeeRepository
) {
@Transactional
fun run() {
val supplier = supplierRepository.save(Suppliers(150, "The High Ground",
"100 Coffee Lane", "Meadows", "CA", "93966"))
coffeeRepository.save(Coffees("Espresso", supplier, 9.99, 0, 0))
println(coffeeRepository.findByName("Espresso").map { it.price })
}
}
fun main(args: Array<String>) {
SpringApplication
.run(JpaSample::class.java, *args)
.getBean(JpaSample::class.java).run()
}
@Transactional
でトランザクション制御、SpringのDIによりAutowireされたRepositoryによりデータベースとのCRUDが実現できる。
SlickでDBIOモナドを合成しなければならない記述に比べると分かりやすい事は分かりやすい。
非同期処理はJPAでは利用できないため、大量のデータを処理する場合などにはScalaでSlick3を用いるほうが良さそうである。
まとめ
職場の状況で仕方なくKotlinに手を出した。
Scalaでは主要なライブラリ群が出揃っており、デファクト・スタンダードに加えオルタナティブも存在している。 Kotlinではこのような状況ではないので、各カテゴリごとにライブラリ選定に多大な時間を要するのではないかと考えていた。 実際には確かにPure Kotlinなライブラリは成熟していないが、Kotlinに対応したJavaのライブラリは多数あり、Javaと共に成熟してきたソフトウェア資産を利用可能であった。 このように、Scalaでのライブラリ選定とは考え方を変えれば、Kotlinで開発しても特に問題は発生しないようである。
複雑なロジックをScalaとKotlinで組み立てようとした場合の文法については…また別の機会に語ることとする。
Footnotes
-
A Brief History of Scala - https://www.artima.com/weblogs/viewpost.jsp?thread#163733 ↩
-
JetBrains readies JVM-based language - https://www.infoworld.com/article/2622405/java/jetbrains-readies-jvm-based-language.html ↩
-
Getting Started - http://slick.lightbend.com/doc/3.2.3/gettingstarted.html ↩
-
Compiler Plugins - Kotlin Programming Language https://kotlinlang.org/docs/reference/compiler-plugins.html ↩