Scala 3.6.2が出た!やったー!
Scala 3.6.2は、Scala 3.6系の最初の正式なリリースで、数多くの機能が追加された。この記事ではそのうち主要な機能を紹介する。また、正確さよりも分かりやすさを優先してザックリ書いているところがあるので、そういうときはブコメや記事で補足してもらえると嬉しい。
- Scala 3.6.2を使う
- 3.6.0と3.6.1との関係
- Clause Interleaving (SIP-47)の安定化
- GivenとContext Boundsの記法の拡充 (SIP-64)
- Context Bounds for Polymorphic Functions
- Add an infix shorthand for Tuple.{Append, Concat}
- まとめ
Scala 3.6.2を使う
Scala CLIでは、-S 3.6.2
オプションをつけるとこのバージョンを使うことができる:
% scala-cli -S 3.6.2
sbtでは、scalaVersion
に3.6.2
を指定してやると良い:
lazy val root = (project in file(".")) .settings( name := "Hello", scalaVersion := "3.6.2" )
3.6.0と3.6.1との関係
Scala 3.6.2はScala 3.6系の最初の正式なリリースと書いた。では3.6.0とか3.6.1は何なのかというと、
- オペミスか何かで作業中のマイルストーンかなにかがpublishされてしまったのが3.6.0(おっちょこちょい)
- さすがにそのままではマズいので最低限動くようにhotfixを当てたのが3.6.1
だった。しかし最低限のhotfixなのでプロダクションでの利用は全く推奨されておらず、まだ使わないで!というステータスになっていたのだ。そしてちゃんとリリースできる状態になって出てきたのが3.6.2というわけで、基本的に3.6.0と3.6.1は欠番扱い。
それでは各種機能について見ていこう。
Clause Interleaving (SIP-47)の安定化
Clause Interleavingという機能が安定したとみなされ、experimentalから本体機能に昇格された。
Clause Interleavingとは、型パラメータを分割して、値パラメータと交互に(interleave)書ける機能。
// AとBが分けられている def pair[A](a: A)[B](b: B): (A, B) = (a, b)
そもそもこれまでのScalaでは、値パラメータリストを分割できた:
def plus0(x: Int, y: Int): Int = x + y plus0(42, 666) def plus(x: Int)(y: Int): Int = x + y plus(42)(666)
パラメータリストを分割することで、後から追加でパラメータを渡すことができるようになり、例えば共通のパラメータを先に渡しておき、残った関数をmap
に渡すような処理で都合が良かった:
def withTax(tax: BigDecimal)(price: BigDecimal) = tax * price val withTax10 = withTax(BigDecimal("1.1")) // 先に税率を決める val prices = Seq(BigDecimal("1000"), BigDecimal("1999"), BigDecimal("500")) prices.map(withTax10) // mapに渡すだけでよい // => List(1100.0, 2198.9, 550.0)
これと同様に、型パラメータも分割して、後から渡せるような仕組みにしよう、というのがClause Interleaving。
この構文があって具体的に何が便利になるかというと、パス依存型、つまり値パラメータに依存して型が決まるようなパターンで便利だ(リリースノート からの受け売り):
trait Key { type Value } trait DB { def getOrElse(k: Key)[V >: k.Value](default: V): V // dependent type parameter }
あるかどうか分からない値を取り出すときにデフォルト値を指定する、というパターンがgetOrElse
だが、その値の型が既に分かっているのではなくて、Key
の中にtype Value
があってこれを利用しなければならない(path-dependent)、というのが上掲のコードだ。Clause InterleavingによってV
の型を指定するのをk: Key
の後ろに延ばしているので、V
型を素直に記述できている。
基本的にこの機能は「従来はヘンなテクニックで記述していたが、素直に書けるようになった」系の機能だと考えておくとよさそう。
GivenとContext Boundsの記法の拡充 (SIP-64)
Context Boundsという機能がある。基本的に型クラスのためにある機能だ。
例えば、以下のような、using
を(Scala 2ではimplicit val
を)使うメソッドがあるとする。using
している型がF[型パラメータで使ってる型]
の形になっているのがミソだ:
def max[A](xs: Seq[A])(using Ordering[A]) = ???
これを、以下のように型: 型クラス
のように書ける機能がContext Boundsだ:
def max[A: Ordering](xs: Seq[A]) = ???
つまり、def foo[A](using F[A])
という形になっている場合はdef foo[A: F]
と書けますよ、というのがContext Boundsだ。なんでこの形だけ優遇されるのかというと、型クラスを利用するときによくこのパターンになるからだ。例えばJSONエンコーダが欲しいとか、トランザクションがあるとか、モノイドになるとか、そういう要求はたいていこの形になる。
これがあるとA
を二度書かなくて良くなり、スッキリした見た目になる。関数を読むときも、「A
があって、それはOrdering
になることができて、xs: Seq[A]
があって・・・」という感じで素直に読める。
ところで、Context Boundsを使うとここではOrdering[A]
には名前が付かないので、このインスタンスを利用したいときはsummon
で呼び付ける必要がある:
def max[A: Ordering](xs: Seq[A]) = {
summon[Ordering[A]]
}
これだと折角スッキリさせたのが勿体ない。そこでSIP-64では、Context Boundsにas
という追加の構文を追加した:
def max[A: Ordering as o](xs: Seq[A]) = {
xs.reduce(o.max)
}
as
を利用すると、その型クラスのインスタンスに名前を付けてすぐに利用できるようになる。つまり以下の構文と全く等価だ:
def max[A](xs: Seq[A])(using o: Ordering[A])
これだけだと単に同じことができるようになっただけだ。しかしSIP-64では同時に複数の型クラスを指定する記法も追加された:
def showMax[X : {Ord, Show}](x: X, y: X): String
もちろん、個々の型クラスに名前を付けることができる:
def showMax[X : {Ord as ordering, Show as show}](x: X, y: X): String = show.asString(ordering.max(x, y))
基本的にこの機能は型クラスを活用している時に非常に便利になる、という感じの機能なので、普段使っていなければ特に憶える必要はない。
Context Bounds for Polymorphic Functions
Polymorphic FunctionsでContext Boundsが使えるようになった。
そもそもPolymorphic Functionsとは、メソッドではない関数でも型パラメータを使えるようになるという非常に便利な機能である。
val pair = [A] => (a: A) => [B] => (b: B) => (a, b) val p = pair(10) p("foo") // => (10, "foo")
Scala 2ではこのようなことはできなかったので、型パラメータを利用したければdef
を使ってメソッドとして実装する必要があった。
そして今回Polymorphic FunctionsでContext Boundsが使えるようになった。つまり以下のようなコードが書けるようになった:
val showMax = [X : {Ord as ordering, Show as show}] => (x: X, y: X) => String => show.asString(ordering.max(x, y))
これによりval
の機能がよりdef
に近づいた。また、関数は匿名関数としてその場に書くことができるので、匿名関数を渡したいときの表現力が上昇したことになる。
Add an infix shorthand for Tuple.{Append, Concat}
Tuple.Append
とTupple.Concat
型に対する便利な表記が追加された。
まず、Scala 3ではタプルの扱いがより柔軟になっていることに留意されたい。Scala 2までではタプルは22-タプルまでしか作ることができなかったが、Scala 3からは無制限となった。また、数が違うタプルとの関連性もより柔軟に定義された。
def addOne[T <: Tuple](t: T) = 1 *: t
例えば上掲のようなメソッドは、タプルを渡すと先頭に1をくっつけた新たなタプルを返す:
addOne(EmptyTuple) // => (1) addOne((42, "foobar", false)) // => (1,42,foobar,false)
またこの例での*:
はタプルのメソッドだが、Scala 3.6.2では、型レベルでの表現力のために:*
と++
が新たにTuple.Append
とTuple.Concat
のエイリアスとして追加された:
import Tuple.{:*} type SB = (String, Boolean) type SBI = SB :* Int // => (String, Boolean, Int)
これは単に型レベルの操作が便利になるね、という感じの機能で、使わない人はぜんぜん使わないと思う。
まとめ
他にも細やかな機能追加や修正が沢山あるが、experimentalな機能追加だったりコンパイラ内部の修正とかが多いのでここでは割愛した。