Swift

Swiftの列挙型はなぜ強いのか

「Swiftの列挙型が強い」という言葉をよく耳にするようになりました。わたしもSwiftの列挙型は好きです。

しかし列挙型はSwift以外にも存在します。そのため、「Swiftの列挙型が強い」という言葉が
列挙型自体が強い(Swiftはどうでもいい)」という意味なのか
Swiftの列挙型が特別強い(ガンガンいこうぜ!)」という意味なのか
はっきりさせなければなりません。

Swiftにおける列挙型とは

The Swift Programming Language (Swift 4.1): Enumerations

Swiftには2種類の列挙型がある。

  • Raw Values
    • 一般的に「列挙型」「enumeration」と呼ばれるものの使い方をする
  • Associated Values
    • 一般的に「algebraic data type(代数的データ型)」「discriminated unions」「tagged unions」「variants」などと呼ばれるものの使い方をする

Raw Valuesの例:

enum CompassPoint: String {
    case north, south, east, west
}

Associated Valuesの例:

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

各列挙子に対し、
事前にデフォルト値(= "raw values")を与えるのがRaw Valuesなやり方で、
後で関連する値(= "associated values"、関連値)を与えるのがAssociated Valuesなやり方。

「Swiftの列挙型が強い」という話題でどちらの種類が挙げられているのかはわからないが、せっかくなので両方とも考えてみる。

Raw Values(いわゆる列挙型)の特徴

Raw Valuesでは各列挙子へ同じ型のデフォルト値を事前に与えなければならない。

Swift Raw Valuesの特徴:

  • protocol RawRepresentable を満たすように自動でメソッドが追加される
    • むしろ RawRepresentable に合わないような宣言をするとエラー
    • -dump-ast または -print-ast でその様子を見ることができる
  • ( RawRepresentable に反しない範囲で)様々な型の値を持つことができる
  • IntString の値を持つ場合、明示的に与えなくても勝手に振ってくれる(「Implicitly Assigned Raw Values」)
  • switchでの列挙子網羅を強制する

C/C++/C#にとっての列挙型は「名前が付いた一連の整数定数」でしかない。

enum Day {
    Sun, Mon, Tue, Wed, Thu, Fri, Sat
};

printf("%d %d %d\n", Sun, Mon, Tue);  // 0 1 2

Kotlinでは各列挙子のことをオブジェクトとみなしている。また、列挙子は列挙型のインスタンスなので以下のように初期化する。

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

println(Color.RED)  // RED
println(Color.RED.rgb)  //  16711680

Scalaでは各列挙子が Val クラスのインスタンスという扱いになっている。これの派生を作ってしまえば列挙子に様々な型の値を持たせることができる。(継承しなくてもOK)

object Planet extends Enumeration {
  protected case class Val(mass: Double, radius: Double) extends super.Val

  val Mercury = Val(3.303e+23, 2.4397e6)
  val Venus   = Val(4.869e+24, 6.0518e6)
  val Earth   = Val(5.976e+24, 6.37814e6)
  val Mars    = Val(6.421e+23, 3.3972e6)
  val Jupiter = Val(1.9e+27, 7.1492e7)
  val Saturn  = Val(5.688e+26, 6.0268e7)
  val Uranus  = Val(8.686e+25, 2.5559e7)
  val Neptune = Val(1.024e+26, 2.4746e7)
}

println(Planet.Mercury) // Mercury
println(Planet.Mercury.mass)  // 3.303E23

TypeScript 列挙型にはnumeric enums、string enumsの2種類がある。このうち、numeric enumsのみ自動で初期値を用意してくれる。

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

関数を用いて列挙子の値を初期化することができたり、コンパイル時にインライン化されて消えてしまうconst enumがあったり、両方の型を持つheterogeneous enumsがあったり、上で紹介した言語ではできないことがいくつかある。

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

console.log(BooleanLikeHeterogeneousEnum.No); // 0

Associated Values(いわゆる代数的データ型)の特徴

Wikipediaに代数的データ型を扱える言語の一覧がある。
Algebraic data type - Wikipedia
これを列挙型(enum)と呼んでいるのは、Swift以外ではRust先輩くらいのものと思われる。他にあったら教えてください。

Swift Associated Valuesの特徴:

  • 再帰的データ構造でもある
    • 再帰も代数的データ型に含むことがある
    • リストなどを作るのに再帰させる必要があるのできっと大事
  • 各列挙子に自動的に静的メソッドが用意される
    • enum HogeEnumswiftc -dump-ast すると(HogeEnum.Type) -> (String) -> HogeEnumというメソッドが増えているのがわかる
  • 例外を除いて非変

静的メソッドとシグネチャが競合するようなメソッドがあるとエラーになってしまう。

enum Barcode {
    case qrCode(String)

    // エラー
    static func qrCode(_ str: String) -> Barcode {
        return Barcode.qrCode(str)
    }
}

Kotlinで同じようなことをするとエラーにならず、 data class の方が呼ばれる。

sealed class Barcode {
    data class QRCode(val str: String): Barcode()

    companion object {
        fun QRCode(str: String): Barcode {
            return Barcode.QRCode(str)
        }
    }
}

Swiftは共変を指定できないし、Swift Associated Valuesも基本的に非変なのだが、 Optional だけ共変になっている。このような特別扱いはstruct Array でも起きている。

class Animal {}
class Cat: Animal {}

let cat0: Optional<Cat> = .some(Cat())
let animal0: Optional<Animal> = cat0

enum MyOptional<T> {
    case some(T)
    case none
}

let cat1: MyOptional<Cat> = .some(Cat())
// エラー
let animal1: MyOptional<Animal> = cat1

共変非変を指定できるScalaではOptionとimmutable.Listに共変( Option[+A]List[+A] )を指定している。mutableのほうは非変。

TypeScriptで代数的データ型を表現するには、「singleton types(要はリテラル)」「union types」「type guards」「type aliases」を使う必要がある。

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

変数や戻り値がnullableであることを示したいだけなら --strictNullChecks オプションとunion types( | )で足りる。

結論?

  • Raw Values(列挙型的使い方)もAssociated Values(代数的データ型的使い方)も既に他言語にあるものなので目新しいものではない
  • どちらの種も言語によって宣言方法やできることが異なる

個人的には機能性と学習コストと自分の目的が釣り合っているSwiftとKotlinくらいがちょうどいいです。できることの多さだけで強さを判断するならScala先輩がトップに見える。

参考文献とか

The Swift Programming Language (Swift 4.1): Enumerations
RawRepresentable - Swift Standard Library | Apple Developer Documentation
SwiftのOptionalはただのenum? - Qiita
SwiftのArrayがミュータブルでもCovariantな理由 - Qiita

Chapter 6. Variants / Real World OCaml
ソフトウェア技法: No.6 (直積型と代数的データ型)

C 列挙体の宣言
列挙型 [C++]
列挙型 (C# プログラミング ガイド) | Microsoft Docs

Enum Classes - Kotlin Programming Language

Scala Standard Library 2.12.4 - scala.Enumeration
列挙型 (enum) が欲しいときの Enumeration と case object... - tnoda-scala
型パラメータと変位指定 · Scala研修テキスト
Scala列挙型メモ(Hishidama's Scala Enumeration Memo)
Effective Scala
ScalaでListが共変でなければいけない理由 - kmizuの日記
scala-hackathon/option.rst at master · yuroyoro/scala-hackathon

Enums · TypeScript
Advanced Types · TypeScript
Union Typesは直和型ではない | 雑記帳
TypeScript、お前もか: nullやundefinedの扱いがイイカゲン過ぎ【事実誤認あり】 - 檜山正幸のキマイラ飼育記
TypeScript、僕が悪かった、ゴメン: nullやundefinedの扱いはマトモだった - 檜山正幸のキマイラ飼育記