Edited at
NewsPicksDay 17

SwiftのEnumで見る代数的データ型ついて

この記事はNewsPicks Advent Calendar 2018の17日目の記事です。


はじめに

皆さんこんにちは。

NewsPicksでソフトウェアエンジニアをしている @kz_moritaです。よろしくお願いします。

NewsPicksには11/1にjoinしまして、約一ヶ月半ほど経ちました。

私は、これまでずっとスマートフォンゲーム開発 (サーバーとクライアント両方) やってきたのですが、現在は心機一転iOSエンジニアとして働かせてもらっています。

初めてのiOS/Swiftということでいろいろキャッチアップ中なのですが、色々触っていく中でSwiftのEnumには面白い点があるなと感じたのでそのことについて書いていこうかなと思います。

まずはじめに、SwiftのEnumについて紹介したのちに、代数的データ型(特に直和型)について書いていこうと思います。


Swift の Enumについて

それではSwiftにおけるEnumがどのようなものなのかを簡単に説明します。

SwiftのEnumを大きく分けると以下の3通りになります。


  • 通常の列挙型Enum

  • 値型のEnum (Raw Value)

  • 関連値を持つEnum (Associated Value)

一つずつ簡単に紹介します。


通常のEnum

いわゆる普通の列挙型です。

enum Color {

case Red, Blue, Green
}


Raw Value

値を持つことができる列挙型です。

enum Name: Type のように定義します。

enum Color: String {

case red = "red"
case blue = "blue"
case green = "green"
}

// rawValueで値にアクセスできる
Color.red.rawValue // => "red"

// 省略もできる

enum Color: String {
case red, blue, green
}

Color.red.rawValue // => "red"


Associated Value

関連値をもつ異なる型を一つのEnum型に持つことができます。

Swiftの公式ページからバーコードの例を引用します。

enum Barcode {

case upc(Int, Int, Int, Int)
case qrCode(String)
}

// Barcode型の変数に.upc, .qrCode両方を代入可能
var productBarcode: Barcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = Barcode.qrCode("ABCDEFGHIJKLMNOP")

// それぞれの値はパターンマッチのようにとりだせる。
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
print("UPC: \(numberSystem)., \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
print("QR code: \(productCode)")
}


これらSwiftのEnumの種類の中でもAssociated Valueが 代数的データ型 (直和型) の性質があるのが非常に良いなと思いました。

続く章で代数的データ型について書いていきます。


代数的データ型とはなにか

代数的データ型とは型同士を組み合わせて作られる型のことで、直積型、直和型、列挙型があります。


  • 直積型 (struct, classなど)

  • 直和型 (Associated ValueのEnum)

  • 列挙型 (通常のEnum)

上記の3つの型についてそれぞれが 取りうる値の数 に注目して見ていこうと思いますが、その前にまず代表的なプリミティブ型の取りうる値について見ていきます。


型が取りうる値の数について

たとえば、Bool 型の場合、True or False のふた通りの値が取り得ます。

これを本記事では以下のように表すことにします。

$

N(Bool) = 2

$

同様に他の型についても見ていくと以下のようになるかと思います。

$N(Int8) = 256$

$N(String) = ∞$

以上を踏まえた上で上記の3つの型についてそれぞれが取りうる値の数について見ていこうと思います。


直積型 (struct)

以下のようなstructを考えます。

struct Sample1 {

let a: Bool
let b: Bool
let c: Bool
}

上記のSample1の取りうる値について列挙すると以下のようになります。

var sample = Sample(a: true, b: true, c: true)

sample = Sample(a: true, b: true, c: false)
sample = Sample(a: true, b: false, c: true)
sample = Sample(a: true, b: false, c: false)
sample = Sample(a: false, b: true, c: true)
sample = Sample(a: false, b: true, c: false)
sample = Sample(a: false, b: false, c: true)
sample = Sample(a: false, b: false, c: false)

合計で8通りあり、以下のような式になります。

$$

N(Sample) = N(Bool) * N(Bool) * N(Bool)

= 2 * 2 * 2

= 8

$$

struct (やclassなど) はそれぞれのメンバ型の取りうる値を集合とみなした場合に、struct自身の取りうる値はメンバ型同士の直積集合をとったものとなります。

参考: Wikipedia 直積集合


直和型 (Associated Type)

以下のようなAssociated TypeのEnumを考えます。

enum SampleEnum {

case sample1(Bool, Bool)
case sample2(Int8)
}

列挙していくと以下のようになります

SampleEnum.sample1(true, true)

SampleEnum.sample1(true, false)
SampleEnum.sample1(false, true)
SampleEnum.sample1(false, false)
SampleEnum.sample2(N) // 128通り

つまり、取りうる値の数は以下のようになります。

$N(.sample1(Bool, Bool)) = 4$

$N(.sample2(Int8)) = 256$

$N(SampleEnum) = N(.sample1) + N(.sample2) = 260$

これはEnumの要素である各々が取りうる値の直和となります。

(.sample1が直積なので、直積の直和ともいえます)

参考: 直和(和集合)


列挙型

それでは通常の列挙型はどうでしょう。

enum Color {

case Red, Blue, Green
}

これらの列挙型の要素Red/Blue/Greenはそれぞれメンバを0個 (引数0のコンストラクタを) もつ型だと見なせます。

つまり上記の直和型でみたものの亜種と考えられそうです。

実際に取りうる値の数を見てみると

$N(Color.Red) = 1$

$N(Color.Blue) = 1$

$N(Color.Green) = 1$

となり、

$N(Color) = N(Color.Red) + N(Color.Blue) + N(Color.Green) = 3$

となります。


直和型のメリット

これまで三つの型を見てきましたが、直和型を有効に使っていくことは堅牢なシステムを構築するにあたって非常に重要だと思っています。

この直和型(Associated Value)のメリットについて書いていきます。

直和型に適している具体的な例としてResultパターンなどが挙げられます。

参考: swiftのResultパターンについて

ここではなにかのAPIなどを叩いて、結果を取得するような例を考えます。

まずは結果を表すstruct (直積型) を考えます。

struct Result<T> {

var value: T?
var error: Error?
}

enum MyError: Error {
case err
}

let res = Result(value: "user1", error: nil)
if let value = res.value {
// valueがあればSuccess
print(value)
}
else if let err = res.error {
// valueがなくerrがあればError
print(err)
}

上記は一見要件を満たしていそうですが、問題もあります。

Result<T>型は .value == nil && .error == nil または、.value != nil && .error != nil という値も取り得てしまいます。

次にこの問題を解決するために、Associated Value (直和型) で表現してみます。

enum Result<T> {

case success(_ value: T)
case failure(_ error: Error)
}

enum MyError: Error {
case err
}

var res = Result.success("user1")
switch res {
case .success(let value):
print(value)
case .failure(let err):
print(err)
}

このようにAssociated Valueで表現することで実際のユースケースと型が一致し、シンプルに要件を満たすことができたかと思います。


まとめ

一般的に型に余分な値が入る余地があるということはそれだけエラーが起きる可能性が増え、またそれを防ぐためのエラーハンドリングが必要になることを意味しています。

直和型をサポートしている言語はそこまで多くありません (Haskell, OCaml, Rust, Elm, TypeScript, C++, etc...) が、

非常に強力な仕組みなのでうまく利用してシンプルかつ堅牢なシステムをつくっていけるようこれらの仕組みをうまく取り入れていきたいと思います。


参考にしたサイト


さいごに

NewsPicksは5年後、世界で最も影響力のある経済メディアになるという目標を実現するための仲間を募集しています。

AdventCalendarでは他にもさまざまな記事を書いているのでぜひお読みいただいて、もし興味がありましたらお気軽にご連絡していただけると幸いです。

AdventCalendarの明日(12/18)の担当は@pakkunで、個人的ベストプラクティスに基づいたWebフレームワークの構成を考えてみたです。お楽しみに!