Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scala 3.6が出たので見所を紹介するよ

Scala 3.6.2が出た!やったー!

github.com

Scala 3.6.2は、Scala 3.6系の最初の正式なリリースで、数多くの機能が追加された。この記事ではそのうち主要な機能を紹介する。また、正確さよりも分かりやすさを優先してザックリ書いているところがあるので、そういうときはブコメや記事で補足してもらえると嬉しい。

Scala 3.6.2を使う

Scala CLIでは、-S 3.6.2オプションをつけるとこのバージョンを使うことができる:

% scala-cli -S 3.6.2

sbtでは、scalaVersion3.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。

xuwei-k.hatenablog.com

この構文があって具体的に何が便利になるかというと、パス依存型、つまり値パラメータに依存して型が決まるようなパターンで便利だ(リリースノート からの受け売り):

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

github.com

基本的にこの機能は型クラスを活用している時に非常に便利になる、という感じの機能なので、普段使っていなければ特に憶える必要はない。

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.AppendTupple.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.AppendTuple.Concatのエイリアスとして追加された:

import Tuple.{:*}
type SB = (String, Boolean)
type SBI = SB :* Int // => (String, Boolean, Int)

これは単に型レベルの操作が便利になるね、という感じの機能で、使わない人はぜんぜん使わないと思う。

まとめ

他にも細やかな機能追加や修正が沢山あるが、experimentalな機能追加だったりコンパイラ内部の修正とかが多いのでここでは割愛した。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?