Scala

Scala2.13の新機能をいくつかピックアップ

Scala2.13はコレクションライブラリの再構築がとにかくでかくてそれ以外はどうなんだろ、、と思ってリリースノートを眺めていたところ意外と色々あるなということがわかった。

主に普段の開発ですぐ使えそうな便利機能を拾ってみました。

リリースノート: https://github.com/scala/scala/releases/tag/v2.13.0

ただし網羅性は保証しません、オレオレピックアップであることにご注意を。


数値


数値リテラルでのアンダースコア区切り

Java7で書けるようになったアレだ。

val n1: Int = 1_000_000   // 1000000

val n2: Double = 3_14E-2 // 3.14


String


String interpolatorでマッチしたところを変数に展開

マッチしないとException発生するので注意。

val s"Hello, $name" = "Hello, James"

println(name) // "James"

val TimeSplitter = "([0-9]+)[.:]([0-9]+)".r
val s"The time is ${TimeSplitter(hours, mins)}" = "The time is 10.50"
println(hours) // 10
println(mins) // 50

val s"$a-$b-$c" = "1-2-3-4-5-6-7-8-9"
println(a) // 1
println(b) // 2
println(c) // 3-4-5-6-7-8-9

val s"$a-$b-$c" = "foo" // scala.MatchError発生


文字列から変換できないケースをOptionで安全表現

変換できないケースを考慮してOption型になるメソッドが追加。

Int以外にもDoubleやBooleanなど色々と。

"123".toIntOption   // Some(123)

"scala".toIntOption // None


Option


when/unless

条件によってSomeにしたりNoneにしたり、がifなしで書けるように。

以下の例は空文字じゃなければSomeに、空文字ならNoneになるようにするもの。

def str2Op(s: String): Option[String] = Option.when(!s.isEmpty)(s)

str2Op("hello") // Some("hello")
str2Op("") // None

なお、whenと条件が逆のunlessもある。

今回の例の場合はunless使えば条件部から ! が消える。


Future


delegateメソッドの追加

メソッド定義は以下。

def delegate[T](body: => Future[T])(implicit executor: ExecutionContext): Future[T]

何が嬉しいんだろう?

以下は例。

常に呼び出し元にはFutureが返ることを保証するために以下のような構造になっているとする。

trait Foo {

def run[A](param: Param)(implicit ec: ExecutionContext): Future[A] = {
// ここでdoRunを呼んで例外発生時はFailureを返す
}
protected def doRun[A](param: Param)(implicit ec: ExecutionContext): Future[A]
}

例外を補足するコード例は以下。

try { doRun(param) } catch { case t: Throwable => Future.failed(t) }

Future.fromTry { Try { doRun(param) } }

Future.unit.flatMap{ _ => doRun(param) }

Future(doRun(param)).flatten

いずれも、try/Tryを使うとかFuture.unitを一度作るか、flattenを使って2重になっているのを打ち消す等と何かひと手間かかっている事がわかる。

delegateはコレを1発で表現できるようにするメソッドだった。

Future.delegate(doRun(param))


コレクション


max/maxBy/min/minByにそれぞれOption型になるメソッドが追加。

val numbers: Seq[Int] = Nil

numbers.max // java.lang.UnsupportedOperationException発生
numbers.maxOption // None


要素数を比較するメソッド

lengthIs が追加になった。

Seq(1,2,3).lengthIs > 5

Seq(1,2,3).lengthIs == 5
// などなど

ここでちょっとした疑問。

以下でやってきたはずだけどわざわざ増やしたのはなんで?

Seq(1,2,3).length > 5

Seq(1,2,3).length == 5

lengthを直接呼ぶと要素数が多い場合に全走査して遅くなる。

それを回避するためにもともと lengthCompare というメソッドを用意していたもののあまり知られてないっぽいのとこのメソッド使いにくいよね、ということでより使いやすい形で定義したよ、というのが背景のよう。

なおlengthIsの先でlengthCompareが呼ばれてます。


distinctBy

distinctはあったが、distinctByが追加されてよりきめ細やかに重複除去ができるように。

Seq(("a", 10), ("b", 20), ("a", 30)).distinctBy(_._1) // Seq(("a", 10), ("b", 20))


partitionMap

Booleanで分割するpartitionはあったが、Eitherを返すpartitionMapが追加。

val (numbers, strings) =

Seq(1, "one", 2, "two", 3, "three").partitionMap {
case n: Int => Left(n)
case s: String => Right(s)
}


unfold

新規追加のメソッド。シグニチャは以下。

def unfold[A, S](init: S)(f: (S) => Option[(A, S)]): List[A]

ちょっとわかりにくい。

例を。以下のようなクラスがあったとする。

case class ResultSetLike[A](private var values: List[A]) {

def hasNext: Boolean = values.lengthIs > 0

def next: A = {
val (head :: tail) = values
values = tail
head
}
}

val resultSetLike = ResultSetLike("A" :: "B" :: "C" :: Nil)

値を取り出し、String値を作ろうと思うと愚直にやるとこんな感じ。

val strBuilder = new scala.collection.mutable.StringBuilder()

while(resultSetLike.hasNext) {
strBuilder.append(resultSetLike.next)
}

val str = strBuilder.toString

これをunfoldを使って書き換えると、、、

val str = 

List.unfold(resultSetLike){ r => if (r.hasNext) Some(r.next -> r) else None }.mkString

となります。


関数


PartialFunction同士のandThen/composeが可能に

これまでandThen/composeは継承元のFunction1のメソッドだった。

PartialFunctionでも定義され、PF同士を合成することが出来るように。

val pf1: PartialFunction[String, Int] = ???

val pf2: PartialFunction[Int, Boolean] = ???
val pf3: PartialFunction[String, Boolean] = pf1 andThen pf2


value discarding警告を回避するUnitの宣言を明示的にする

関数でしか使えないわけではないけどもよく使うのは関数かなと。

以下のコードはコンパイルエラーにならずに通る。

def foo(): Unit = {

100
}

value discardingというScala言語の機能で以下のようになってる。

def foo(): Unit = { 

100
()
}

この挙動を嫌ってコンパイルエラーにしたい場合は、コンパイラオプションの指定によって振る舞いを変更できる。


build.sbt

scalacOptions ++= Seq(

"-Xfatal-warnings",
"-Ywarn-value-discard"
)

当然この設定をすると以下のようなコードを通すには明示的に '()' を最後に返すコードを自分で書く必要がある。毎度コレを書くのもなぁ。

def do(): Int = ???

def foo(): Unit = {
......
do()
()
}

前置きが長くなったが、そんな時は2.13でサポートされた以下の記述でちょっとだけシュッと書けるようになった。

def foo(): Unit = {

......
do(): Unit
}


その他


tap/pipe

Optionだとmapで変換関数を渡し、foreachでUnitな副作用関数を渡す、といった処理順で記述することが出来るわけですが、

val f = (_:Int) * 10

Option(1 + 2 + 3).map(f).foreach(println)

一方、これが生のIntになると処理順で記述しようとすると変数にとった複数行に渡る記述になるし、一行で記述しようとすると処理順とは逆の記述をしないといけなくなる。

val n1 = 1 + 2 + 3

val n2 = f(n1)
println(n2)

// もしくは
println(f(1 + 2 + 3))

pipe/tapを使うと以下のように記述できるようになる。

import scala.util.chaining._

(1 + 2 + 3).pipe(f).tap(println)

なお、tapをOption#foreachと対比させるような記述をしましたが、tapの戻り値はUnitではなく入力値を返すことに注意。

def tap[U](f: A => U): A = {

f(self)
self
}


Using

リソースの自動リリース(Close処理)が可能。

java.lang.AutoCloseable を持ったリソースを対象とする。

val lines: Try[Seq[String]] =

Using(new BufferedReader(new FileReader("file1.txt"))) { reader =>
import scala.jdk.StreamConverters._
reader.lines.toScala(List)
}

複数リソースを伴う処理はこんな感じ。

val lines2: Try[Seq[String]] =

Using.Manager { use =>
val r1 = use { new BufferedReader(new FileReader("file1.txt")) }
val r2 = use { new BufferedReader(new FileReader("file2.txt")) }

r1.lines.toScala(List) ::: r2.lines().toScala(List)
}


unusedアノテーション

scalacオプションに「-Xfatal-warnings -Ywarn-unused」を設定していると使用していない変数について警告を出してくれる。

ただし中には使用を忘れているわけではないものも警告が出てしまう問題があった。

(例として以下のような例外の種類によってリトライするかどうかを指定できる仕組みの実装において通常はリトライはしない、みたいなデフォルトの設定をするような時)

trait Base {

protected def isRetry(t: Throwable): Boolean = false

......
}

こういう場合はunusedアノテーションを使うことで意図的に使ってないことを宣言でき、警告を出さないようにできる。

import annotation.unused

protected def isRetry(@unused t: Throwable): Boolean = false