MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

Scalaのリテラル型について調べてみた!

サーバサイドエンジニアの飛田です。 主にDSPの開発を行っています。

今回の記事では、Scalaのリテラル型について調査しましたので、 ここで共有させていただきます。

リテラル型

SIP-23 - Literal-based singleton types | Scala Documentation

リテラル型とは、特定の値だけを取り扱うことができるような型です。

例えば、"aaa"という値しか受け付けないA型というものを定義してやると、 "aaa"A型として取り扱うことができますが、"bbb"のような値はA型として扱うことができません。

例:

scala> type A = "aaa"
type A

scala> val a: A = "aaa"
val a: A = aaa

scala> val b: A = "bbb"
                  ^
       error: type mismatch;
        found   : String("bbb")
        required: A
           (which expands to)  "aaa"

また、上記のAのような文字列"aaa"から定義したリテラル型はString型として扱うことができます。

scala> val literal: "aaa" = "aaa"
val literal: "aaa" = aaa

scala> val string: String = literal
val string: String = aaa

一方で、String型の値 "aaa"は、"aaa"型として扱うことができません。

scala> val string: String = "aaa"
val string: String = aaa

scala> val literal: "aaa" = string
                            ^
       error: type mismatch;
        found   : string.type (with underlying type String)
        required: "aaa"

IntBooleanについても同様です。

Int型とリテラル型:

scala> val literal: 1 = 1
val literal: 1 = 1

scala> val integer: Int = literal
val integer: Int = 1
scala> val integer: Int = 1
val integer: Int = 1

scala> val literal: 1 = integer
                        ^
       error: type mismatch;
        found   : integer.type (with underlying type Int)
        required: 1

Boolean型とリテラル型:

scala> val literal: true = true
val literal: true = true

scala> val bool: Boolean = literal
val bool: Boolean = true
scala> val bool: Boolean = true
val bool: Boolean = true

scala> val literal: true = bool
                           ^
       error: type mismatch;
        found   : bool.type (with underlying type Boolean)
        required: true

使用例

定数

リテラル型ですが、Scala2.13ですと、定数を定義する際に利用することができそうです。

scala> val constant: "c" = "c"
val constant: "c" = c

なお、val constant: "c"のようにリテラル型を明記しなくとも、 単にfinalをつけるだけで変数がリテラル型の値として取り扱われます。

final val constant = "c" // リテラル型

シンボル型の代わりとして

ScalaのSymbolクラスが将来的に標準ライブラリから除去されるらしいので、 Symbol型の代わりとしてリテラル型を使えそうです。

scala> val soba = Symbol("Soba")
val soba: Symbol = Symbol(Soba)

scala> val udon: "Udon" = "Udon"
val udon: "Udon" = Udon

Scala3のドキュメントより引用:

Although the Symbol class is useful during the transition, beware that it is deprecated and will be removed from the scala-library in a future version. You are recommended, as a second step, to replace every use of Symbol with a plain string literals "abc" or a custom dedicated class.

Scala3のドキュメントのほうでは、Symbolの代わりにただのStringのリテラルを使うことが書かれていますが、文字列のリテラル型も使うのもありかと思いました。

ユニオン型と組み合わせて使う

Scala3ですと|を使ってユニオン型を表現できるため、
以下のように、引数に特定の定数しか受け付けないような関数を定義できます。

// Scala3
def isRed(color: "red" | "green" | "yellow"): Boolean = color == "red"

また、IntelliJ IDEAですと、引数の候補を以下の画像のようにわかりやすく警告してくれるので、IDEによる実装支援が得られるというメリットもあるかと思います。

f:id:tobita_yoshiki:20220224172630p:plain

おわりに

Scala3ですと、|でユニオン型が定義できるため、リテラル型はかなり使えそうな機能かなと思いました。

Scala3より古いバージョンのScalaですと、定数としてしか使えなさそうな印象を受けました。 依存型やShapelessなどの型を駆使した機能やライブラリを使わないと、 リテラル型を自前実装で使用するといったことはあまりなさそうかなといった印象です。

以上、参考になれば幸いです!