Lambdaカクテル

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

Invite link for Scalaわいわいランド

Scala3のUnion Types / Intersection Typesを試してみた

Scala3チャレンジをする記事の二本目。

前回はこちら。

blog.3qe.us

ソースコードは以下に置いてある。

github.com

github.com

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つ以上の型からより狭い型を作るためのもの。前項と同様に集合のアナロジーで考えると共通集合を取っているのと同じである。

やや天下り的な例だが、EmptySemigroupとを定義することで、この両方の性質を持つ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]みたいなことを書かずに済んでいるし、その他の継承構造から余計なメソッドやフィールドが流れてきてしまうことも防げている。この例で言うとStringMonoidInstancenameといったフィールドを仮に持っていたとしても、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が使えるようになっているので↑みたいな型付けの導出が書けるようになったんだって。

github.com

\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}

が出てくるよ。

kivantium.hateblo.jp

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