Scala3チャレンジをする記事の二本目。
前回はこちら。
ソースコードは以下に置いてある。
Union Types
Scala 3では、A | B
と書くことで型と型をひっつけて、「そのどっちかの型」を表現する型を簡単に書けるようになった。型を値の集合だと見ると、和集合を取っているのと同じである。なのでUnion Typesと呼ぶ。
case class Circle(r: Double) case class Box(h: Double, w: Double) def area(shape: Circle | Box): Double = shape match case Circle(r) => Math.PI * (r * r) case Box(h, w) => h * w
このようにUnion Typesに対してパターンマッチを行うと、ちゃんとtype exhaustive checkが走って「お前ちゃんと網羅できてないぞ」と警告を出せる。
case class Circle(r: Double) case class Box(h: Double, w: Double) def area(shape: Circle | Box): Double = shape match case Circle(r) => Math.PI * (r * r)
[warn] -- [E029] Pattern Match Exhaustivity Warning: /home/windymelt/src/github.com/windymelt/scala-3-exercise/src/main/scala/Union.scala:12:38 [warn] 12 | def area(x: Box | Circle): Double = x match [warn] | ^ [warn] | match may not be exhaustive. [warn] | [warn] | It would fail on pattern case: Box(_, _) [warn] | [warn] | longer explanation available when compiling with `-explain` [warn] one warning found
Scala3のコンパイラはかなり親切になっていて、具体的にどこがコケてるとかがかなり詳しく表示される。Rustとかのコンパイラが丁寧な言語の影響を良い意味で受けてそうなイメージがある。
Eitherとの違い
おいそれってYO!Either
と同じじゃんかと思われる読者諸賢もおられるかもしれない。Union TypesはEither
と違って3つ以上の型の合併も可能である。
type ISB = Int | String | Boolean
そのかわり高級なメソッドの類は(たぶん)用意されていないので、うまく使いわけよう。
Discriminated Union Typesもどき
ところで、TypeScriptでリテラル型を使ってやるようなDiscriminated Union Typesはやれるのかなあ?と思って書いてみたところダメだった。
def distinguish(x: Discriminated1 | Discriminated2): Unit = if (x.t == "type1".asInstanceOf["type1"]) // ifを使ってもtype narrowingは起こらない x.only1 // doesn't compile trait HaveLiteral: type Tag val t: Tag case class Discriminated1() extends HaveLiteral: type Tag = "type1" val t: "type1" = "type1" def only1: Unit = println("1にしかないメソッド") case class Discriminated2() extends HaveLiteral: type Tag = "type2" val t: "type2" = "type2"
まあ普通に型でmatch
するしかなさそう。Discriminated Union Typesが使えると、JSONから取り出したデータをいじるようなときに便利かなと思う。
型付け規則は以下の通り:
\begin{prooftree} \AxiomC{$A$} \AxiomC{$B$} \BinaryInfC{$A <: A | B$} \end{prooftree} \begin{prooftree} \AxiomC{$A <: T$} \AxiomC{$B <: T$} \BinaryInfC{$A | B <: T$} \end{prooftree}
また、以下の2つは後述のIntersection Typesで使う&
に入れ替えても成り立つ:
\begin{prooftree}
\AxiomC{$A | B$}
\UnaryInfC{$B | A$}
\end{prooftree}
\begin{prooftree}
\AxiomC{$A | (B | C)$}
\UnaryInfC{$(A | B) | C$}
\end{prooftree}
Intersection Types
Intersection TypesとはUnion Typesの逆で、A & B
と書くことで2つ以上の型からより狭い型を作るためのもの。前項と同様に集合のアナロジーで考えると共通集合を取っているのと同じである。
やや天下り的な例だが、Empty
とSemigroup
とを定義することで、この両方の性質を持つMonoid
を定義することができる:
trait Empty[A]: def empty: A trait Semigroup[A]: def combine(x: A, y: A): A class StringMonoidInstance extends Empty[String] with Semigroup[String]: def combine(x: String, y: String): String = x ++ y def empty = "" type Monoid[A] = Empty[A] & Semigroup[A] // emptyとcombineとの両方を持つ型 // やや強引だが、EmptyかつSemigroupな型であるMonoidを手で受け取ってreduceを実装してみる def reduce[A](xs: Seq[A], instance: Monoid[A]): A = xs.foldLeft(instance.empty){ case (b, a) => instance.combine(b, a) } println(reduce(Seq("foo", "bar", "buzz"), StringMonoidInstance())) // => "foobarbuzz"
素直に2つの型の共通部分を取れるので、trait Monoid[A] extends Empty[A] with Semigroup[A]
みたいなことを書かずに済んでいるし、その他の継承構造から余計なメソッドやフィールドが流れてきてしまうことも防げている。この例で言うとStringMonoidInstance
がname
といったフィールドを仮に持っていたとしても、reduce
メソッドからはそれは見えない。
型付け規則は以下の通り:
\begin{prooftree} \AxiomC{$T <: A$} \AxiomC{$T <: B$} \BinaryInfC{$T <: A \& B$} \end{prooftree} \begin{prooftree} \AxiomC{$A <: T$} \UnaryInfC{$A \& B <: T$} \end{prooftree} \begin{prooftree} \AxiomC{$B <: T$} \UnaryInfC{$A \& B <: T$} \end{prooftree}
ちなみにUnion TypesはIntersection Typesを分配する: \begin{prooftree} \AxiomC{$A \& (B | C)$} \UnaryInfC{$A \& B | A \& C$} \end{prooftree}
まとめ
結構基礎的な型の話なので、急に役立つという雰囲気でもなく、へ〜良さそう、くらいの雰囲気だった。TypeScriptにあったあのUnion Typesが入って嬉しい〜という気持ち。
MathJax v3以降ではbussproofs.styが使える
Mathjax v3以降ではbussproofs.styが使えるようになっているので↑みたいな型付けの導出が書けるようになったんだって。
\begin{prooftree} \AxiomC{$T <: A$} \AxiomC{$T <: B$} \BinaryInfC{$T <: A \\& B$} \end{prooftree}
って書くと
\begin{prooftree} \AxiomC{$T <: A$} \AxiomC{$T <: B$} \BinaryInfC{$T <: A \& B$} \end{prooftree}
が出てくるよ。