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
となります。
foreachEntry
foreach
と違ってFunction2をとるので、 case (k,v) =>
ってやらずに以下のようにcase
なしで使える。
Map(1->"one", 2->"two").foreachEntry { (k,v) =>
println(s"key is $k, value is $v")
}
関数
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
()
}
この挙動を嫌ってコンパイルエラーにしたい場合は、コンパイラオプションの指定によって振る舞いを変更できる。
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
Product
productElementNameおよびproductElementNamesが追加になった。こんな感じでケースクラスのフィールド名が取れるようになった。
case class Param(token: String, userId: String) {
def urlParam =
(productElementNames zip productIterator).map{ case (k, v) => s"$k=$v" }.mkString("&")
}
Param("xxxx-yyyy-zzzz", "mtoyoshi").urlParam // token=xxxx-yyyy-zzzz&userId=mtoyoshi