LoginSignup
93
45

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-09

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
  () 
}

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

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

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
93
45
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
93
45