KotlinプログラマのためのScala入門(2)〜発展編〜

前の記事では、主にScalaとKotlinの類似点にフォーカスして、基本部分に関して大きな違いがないことを示しました。

ただ、これだけですと、Kotlinでいいじゃんとなりかねないので、この記事ではScalaならではのメリットについて語りたいと思います。

標準ライブラリでの不変コレクションサポート

Kotlinでは、 java.util パッケージのコレクションをほぼ再利用しているため、フットプリントが軽いというメリットがある一方、不変性を保証できないという欠点があります。

たとえば、

class Foo {
    var myList: List<String>? = null
    fun hoge(list: List<String>) {
        myList = list
    }
}

というコード(わざとらしいですが)があったとき、フィールド myListlist を格納することは危険を伴います。 List<String> 自体は読み取り専用でしかなく、書き込み可能な List と同じ参照を指している危険性があります。この例はわざとらしいですが、安全にコレクションを渡し回せるかどうかは安心感に大きく影響します。

一方、Scalaにおける同等のコード

class Foo {
    var myList: Option[List[String]] = None
    fun hoge(list: List[String]) {
        myList = Some(list)
    }
}

では、 List[String] は、不変オブジェクトであることが保証されているため、フィールドに格納しようが、他のモジュールに渡そうが安全であることが 確信 できます。この確信は、オブジェクトをあちこちに安全に渡せること、防御的なコピーが必要ないこと、という形で効いてきます。

これだけだと、Javaのコレクションとの連携において不安を感じる向きがあるかもしれませんが、

import scala.collection.JavaConverters._

とするだけで、 asScalaasJava といったメソッドが追加され(Kotlinにおける拡張メソッドに相当します)、Java側にコレクションを渡すのにはさほど不自由しません。

implicit parameterによる重複コードの削減

Scalaにおけるimplicitというのは誤解されがちですが、本来はコード削減のための有用な手段です。たとえば、Scalaのコレクションには、 sum メソッドがあり、

List(1, 2, 3).sum // 6

のようにして使えますが、 sum のコード本体は一つだけで、 scala.math.Numeric を継承したimplicitなオブジェクトがあればあらゆる型に適用できます。

一方で、Kotlinでは、このようなコードの共通化ができないため、たとえば、配列の型ごとに sum実装が重複する羽目になっています。

implicit parameterは主にライブラリ製作者にとって便利な機能ですが、便利なライブラリを作ることができる能力というのは、大きな差をもたらします。型クラスのより発展的な使い方については、手前味噌ですが、ここなどを参照してみてください。

記号メソッドとfor式とパターンマッチを用いたDSLの構築

Scalaでは記号メソッドの濫用が批判されていることもあり、一概にいいことだと断定できませんが、用途によっては極めて便利なことも確かです。

たとえば、以下は、拙作 scomb のJSONのサンプルですが、簡潔に、JSONの文法を表現することができています。

    lazy val jvalue: P[JValue] = rule(jobject | jarray | jstring | jnumber | jboolean | jnull)

    lazy val jobject: P[JValue] = rule{for {
      _ <- LBRACE
      properties <- pair.repeat0By(COMMA)
      _ <- RBRACE
    } yield JObject(properties:_*)}

    lazy val pair: P[(String, JValue)] = rule{for {
      key <- string
      _ <- COLON
      value <- jvalue
    } yield (key, value)}

    lazy val jarray: P[JValue] = rule{for {
      _ <- LBRACKET
      elements <- jvalue.repeat0By(COMMA)
      _ <- RBRACKET
    } yield JArray(elements:_*)}

    lazy val string: Parser[String] = rule{for {
      _ <- $("\"")
      contents <- ($("\\") ~ any ^^ { case _ ~ ch => escape(ch).toString} | except('"')).*
      _ <- $("\"")
      _ <- space.*
    } yield contents.mkString}

    lazy val jstring: Parser[JValue] = rule(string ^^ {v => JString(v)})

    lazy val jnumber: Parser[JValue] = rule{for {
      value <- (set('0'to'9').+) ^^ { case digits => JNumber(digits.mkString.toInt) }
      _ <- space.*
    } yield value}

    lazy val jboolean: Parser[JValue] = rule(
      TRUE ^^ {_ => JBoolean(true)}
    | FALSE ^^ {_ => JBoolean(false)}
    )

    lazy val jnull: Parser[JValue] = rule(NULL ^^ {_ => JNull})

このような簡潔な記述には、

  • パターンマッチング( { case ... } の部分)
  • for式
  • 記号的メソッドを自由に使える

という点が貢献しています。パーザDSLを自分で作る機会は稀ですが、その他の用途でEDSL(埋め込みDSL)を作る機会は少なくなく、このような点はScalaで色々な事を簡潔に表現する事に役立っています。

実は、筆者は、Kotlinで同様のパーザライブラリを作ってみたのですが、どうしても記述がScalaと比較して冗長にならざるを得ませんでした。特にパターンマッチとfor式がないことが問題でした。

sbtのエコシステム

sbtは当初はSimple Build Toolの略でしたが、今はScala Build Toolの略であり、習得の難しさから、simpleでないといったことが言われます。一方で、sbtは非常にプラグインが充実しており、

project/plugins.sbt

addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.4")

と書くだけでプラグインを利用できるお手軽さがあります。ここには、sbtプラグインの情報が集約されていますが、非常に多種多用なプラグインが利用できることがわかります。また、サードパーティによるプラグイン開発が非常に盛んであることも特徴の一つと言えます。

ちなみに、例として出したsbt-releaseプラグインは、プロダクトのリリースを自動化するsbtプラグインで、これとsbt-sonatypeプラグインを組み合わせることで、maven centralへのライブラリのpublishと各種リリース作業を一度にすることができ、非常に重宝しています。

for式の便利さ

for式は主にモナド的な操作を簡単に書くためのシンタックスシュガーといえますが、そのようなことを知らなくても、便利に使うことができます。たとえば、次のような型を定義します。

case class User(id: Long, name: String, password: String)
sealed trait LoginError
// パスワードが間違っている場合のエラー
case object InvalidPassword extends LoginError
// nameで指定されたユーザーが見つからない場合のエラー
case object UserNotFound extends LoginError
// パスワードがロックされている場合のエラー
case object PasswordLocked extends LoginError

これは、(何らかのシステムへの)ログインが失敗した場合のエラーをあらわす代数的データ型です。Userは最終的に、ログインが成功したときに返されるデータ型とします。

これを、 Either 型を使って、 Either[LoginError, ResultN] として表現すると、for式で次のように表現することができます。

for {
  r1 <- loginProcess1(...)
  r2 <- loginProcess2(...)
  r3 <- loginProcess3(...)
} yield createUser(r1, r2, r3)

このように書くと、途中で LoginError が起きたとき、全体が失敗してくれます(ただし、Scala 2.12以降)。この例は極度に単純化したものですが、Scalaではエラー処理で Either は普通に使われます。処理が連続して、途中が失敗した場合に全体が失敗するという構造は色々なところで見つけることができ、非常に便利なものです。

まとめ

Scalaは、Kotlinと比較したとき

  • for式
  • implicit parameter
  • sbt/Scalaのエコシステム
  • DSL構築に向いた機能(記号メソッドやパターンマッチ)

といった点に特徴があると言えそうです。特に、Scalaを日常的に使っていて便利に感じるのは、for式とパターンマッチで、これがあるとないとでは快適さが大きく違っていると感じます。

また、これらの特徴の一部は、Kotlinにライブラリとして輸入することも可能であり、その意味でもKotlinプログラマがScalaを学ぶ価値はあると思います。

私の力不足でScalaの魅力を伝えきれていないかもしれませんが、この機会にScalaに挑戦してみてはいかがでしょうか。

なお、Scalaは、昔はドキュメントの不足が指摘されていましたが、今は

https://docs.scala-lang.org/

に集約されており、以前よりだいぶ学びやすくなっています。