ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

6. レコード構文と多相型とカインドと :: Swift プログラマのための Haskell 入門

さて、前回は Haskell のリストや再帰処理などを見てきました。

今回はこれまで見てきたデータ型について軽く復習した上で、より詳細について見ていきます。

直積と直和

Haskell では直積と直和のデータを扱えるのでした。

Swift では直積については構造体、直和については enum を使用して定義できますが、 Haskell ではどちらも data キーワードを使って定義します。

また値を取り出す際、Swift では直積についてはプロパティ、直和については switch (など)によるパターンマッチを使用しますが、Haskell では常にパターンマッチを使用してデータを取り出します。

直積データ

まず、直積は データの組み合わせ というべきもので、Swift で言えばタプルや構造体に当たるものです。

2つの Int 型の数値を持つ Point 型は以下のように定義できます。

data Point = Point Int Int

Swift では構造体または typealias でタプルに別名をつけるのに似ています(タプルについては少し異なるのですが、それについては後述します)。

// Swift
struct Point {
    var x: Int
    var y: Int
}

typealias Point = (Int, Int) // typealias でタプルに別名をつける

値を取り出したい場合、Haskell ではパターンマッチによって取り出します。

以下は Point 型を受け取って、その座標を x, y という形式の文字列に変換する関数です。

pointToString :: Point -> String
pointToString (Point x y) = (show x) ++ ", " ++ (show y)

Swift では以下のように書けるでしょう。

func pointToString(_ p: Point) {
    String(p.x) + ", " + String(p.y)
}

今回の Point 型は2つの値しか保持していないため分かりやすいですが、Swift におけるプロパティのように名前をつけられないのは不便だと感じるかもしれません。

3. データ構造でも軽く触れましたが、Haskell にはレコード構文というものが用意されており、それを使用することでフィールドに名前をつけられるようになりますが、それについては後述したいと思います。

直和データ

さて、直和は いずれかの値 を表現するデータ型で、Swift では enum に相当するものでした。

白また黒を表現する Color は以下のように定義できます。

-- Haskell
data Color = Black | White

また、Swift の enum のように関連する値を持つことができます。

Swift の Optional 型にあたる Maybe 型は、Haskell では以下のように定義されています。

-- Haskell
data Maybe a = Nothing | Just a

a型変数と呼ばれるもので、Swift のジェネリックパラメータに当たるものです。

それぞれ Swift では以下のように定義できるでしょう。

// Swift
enum Color {
    case black
    case white
}

enum Maybe<T> {
    case nothing
    case just(T)
}

直和のデータを判断する場合もパターンマッチを使用します。

以下は Maybe String 型を受け取り、値がなかった場合は ”None" 、 値があった場合は ”Found <値>” という文字列に変換する関数です。

-- Haskell
hey :: Maybe String -> String
hey Nothing       = "None"
hey (Just string) = "Found " ++ string

Swift では以下のように書けるでしょう。

// Swift
func hey(_ maybe: Maybe<Int>) -> String {
    switch maybe {
    case .nothing:          return "None"
    case .just(let string): return "Found " + string
    }
}

なお、あとから参照する必要のない変数は、Swift と同じように _ で捨てることができます。

-- Haskell
hey :: Maybe String -> String
hey Nothing  = "None"
hey (Just _) = "Found!"

直積と直和の組み合わせ

当然ながら直積と直和は、組み合わせて使用できます。

以下は、長方形を表現する Rectangle または正方形を表現する Square からなる Shape 型を表現した Haskell のコードです。

-- Haskell
data Shape = Rectangle Point Int Int
           | Square Point Int

Swift では enum と構造体を組み合わせて以下のように定義することもできますし、

// Swift
enum Shape {
    case rectangle(Rectangle)
    case square(Square)
}

struct Rectangle {
    var point: Point
    var width: Int
    var height: Int
}

struct Square {
    var point: Point
    var width: Int
}

以下のように enum だけで定義することもできます。

// Swift
enum Shape {
    case rectangle(Point, Int, Int)
    case square(Point, Int)
}

ここまで便宜的に、Swiftにおいては直積は構造体、直和は enum に対応すると説明してきましたが、このようにコードの対応関係をみてみると、Haskell における data に対応するのは enum であり、どちらも直積と直和を表現することが可能であると分かります。

言い方を変えると、単に直積と直和に基づいたデータ構造を定義するだけであれば、Swift は enum だけで表現できることが可能なのです。

レコード構文

さて、3. データ構造 でも軽く触れましたが、Haskell では レコード構文 というものを利用して、Swift の構造体のように各フィールドに名前をつけることができます。

以下はさきほどの Point をレコード構文で記述したものです。

-- Haskell
data Point = Point { x :: Int, y :: Int }

このように定義すると以下のようなことが可能になります。

-- フィールド名を指定して生成
Point { x = 1, y = 2 }

-- フィールド x を取得
x point

-- フィールド y を取得
y point

-- `point`のx座標だけを変更した新しい値を取得
updatePoint :: Point -> Point
updatePoint point = point { x = 3 }

Swift との大きな違いとして、フィールドとして定義された xy は単なる関数として実装されている点です。point.xpoint.y などと書きたくなりますが、x pointy point などのように記述します。

コンパイラが、以下に相当するようなコードを自動生成してくれるだけと考えると分かりやすいでしょう。

x :: Point -> Int
x (Point x' _) = x'

y :: Point -> Int
y (Point _ y') = y'

-- `point { x = newX }` に相当
updateX :: Point -> Int -> Point
updateX (Point _ y) newX = Point newX y

-- `point { y = newY }` に相当
updateY :: Point -> Int -> Point
updateY (Point x _) newY = Point x newY
  • レコードという特別な構造が存在するわけでなく、構造としては直積のデータ型と変わらない。
  • Haskell において値を取り出すのは常にパターンマッチである。

という、2点を抑えておくと良いでしょう。

なお、勘の良い方は xy といった関数名が簡単に重複してしまうことに気づいたかもしれません。

例えば、以下のコードは name という関数が同じ名前空間で重複するためコンパイルエラーになります。

data Person = Person { name :: String }
data Dog = Dog { name :: String }

最もシンプルな解決方法は personNamedogName といったように異なるフィールド名をつけることですが、さすがにそれはやってられないと思うことでしょう。

これの解決方法としては GHC の言語拡張を利用したり、Lens といったライブラリを使用する方法があるのですが、それについてはまたの機会に取り上げたいと思います。

type / newtype

ここまで data キーワードを利用して新しいデータ型を定義してきましたが、Haskell では型を定義するのに typenewtype というキーワードも存在します。

まず、 type は Swift における typealias と同様のもので、単にエイリアス(別名)をつけるだけの機能で、Haskell においては 型シノニム と呼ばれています。

-- Haskell
type BookID = String
// Swift
typealias BookID = String

単なるエイリアスなので元の型と区別されない点も Swift と同様です。

-- Haskell
incrementID :: BookID -> BookID
incrementID = ...

incrementID "42" -- String型をそのまま渡せる
// Swift
func incrementID(_ id: BookID) -> BookID { ... }

incrementID("42") // String型をそのまま渡せる

これはコードを読みやすくしますが、型を明確に区別して扱いたい場合には利用できません。

既存の型と区別したい場合には、newtype キーワードを利用します。

newtype BookID = BookID String

incrementID :: BookID -> BookID
incrementID = ...

incrementID (BookID "42") -- BookID 型にしないと渡せない

Swift では新しい構造体でラップすることで、同様のことが実現できます。

struct BookID {
    var rawValue: String
}
  • 単に別の名前をつけたい場合は type
  • 既存の型をラップした新しい型を定義したい場合は newtype
  • 自分で新しいデータ型を定義したい場合は data

と覚えておくとよいでしょう。

データ型の宣言の意味

さて、このあたりでデータ型の宣言をするコードの正確な意味を見てみたいと思います。

さきほどの、Shape 型の例を見てみます。

data Shape = Rectangle Point Int Int
           | Square Point Int

これは以下のような構文で成り立っています。

data {新しい型名} = {値コンストラクタ1} {フィールド1} {フィールド2} ...
                | {値コンストラクタ2} {フィールド1} {フィールド2} ...
                | ...

Shape の例に当てはめると以下のようになります。

  • 新しく Shape 型を宣言している。
  • Shepe 型は、値コンストラクタ Rectangle または値コンストラクタ Square から生成できる。
  • RectanglePointIntInt からなる。
  • SquarePointInt からなる。

値コンストラクタという用語は聞き慣れないかと思いますが、Swift や他言語におけるコンストラクタとほぼ同じ意味になります。

すなわち、Rectangle という値コンストラクタは PointIntInt を受け取ることで、Shape 型の値が生成できる、ということです。

shape :: Shape
shape = Rectangle (Point 1 2) 4 8 -- `Rectangle`に3つの値を与えることで`Shape`型になる

「なんだか関数のようだ」と感じた人も居るかもしれません。

実のところ値コンストラクタの実体は単なる関数で、REPL 上で型を調べることでそれが分かります(:t で型を調べることができます)。

> :t Rectangle 
Rectangle :: Point -> Int -> Int -> Shape

他の関数と同じくカリー化されており部分適用も可能になっています。

makeRectangleToOrigin :: Int -> Int -> Shape
makeRectangleToOrigin = Rectangle (Point 0 0)

さて、そうなると「単に コンストラクタ という命名ではダメなのか?」と疑問に持つのは当然のことです。

実は Haskell には型コンストラクタという用語が存在するためそれと区別する必要があるのです。

多相型

それを理解するために Maybe 型の例を見てみます。

data Maybe a = Nothing | Just a

Swift におけるジェネリックス型のように、中身に任意の型を格納できる型を多相型と読んだりします。

この例では Maybe aa の部分に任意の型を当てはめることができ、Maybe IntMaybe String といったデータ型を利用できます。

これは以下のような構文で成り立っています。

data {新しい型名} {型変数1} {型変数2} ... = ...

つまり、以下のように読むことが出来ます。

  • Maybe という新しい型を定義している。
  • それは型変数 a で任意の型を受け取る。

さて、ここで重要なのが Maybe IntMaybe String といった型は使えるものの、 型変数 a が埋まっていない Maybe という型は使えない ということです。

ややこしく聞こえるかもしれませんが、Swift で Optional という型をそのまま使えないのと同じことです。

// Swift
let x: Optional<Int>    = .none
let y: Optional<String> = .none
let z: Optional         = .none // 中身の型が確定していないのでコンパイルできない

つまり、Maybe そのままでは利用できないけれど、IntString といった型を1つ渡せば Maybe IntMaybe String といった型として利用できるということです。

やや話の誘導が露骨だったようにも感じますが、お察しのとおり Maybe のように何らかの型を受け取って利用できるようになるものを Haskell では 型コンストラクタ と呼んだりします。

必要な値を受け取って値を生成するから「値コンストラクタ」 、必要な型を受け取って型を生成するから「型コンストラクタ」 ということですね。

カインド

さて、Maybe1つの型を受け取ることで具体型になる型コンストラクタなのでしたが、2つの型からなる Either a b というデータ型も存在します。

data Either a b = Left a
                | Right b

これは Swift における Result<Sucess, Failure> と大体似たようなもので、成功を Right 、失敗を Left で表現するものになっています。「成功時の値」と「失敗時の値」の型が(通常は)異なるため、2つの型変数 ab を扱うようになっているのですね。

さて、型変数を引数として見なすと、型コンストラクタは型を生成するための関数のようにも見えてくるかもしれません。

すなわち以下のような関数とみなせないでしょうか?

a -> Maybe a
a -> b -> Either a b

Maybea という具体型を受け取って Maybe a という具体型になり、Eithera という具体型と b という具体型を受け取って Either a b という具体型になる、という考え方です。

実は Haskell のコンパイル時の型チェックにおいて、上記のような計算が行われています。そのようなチェックを行うためには、 IntMaybeEither異なる型として区別する必要があります。そのような型の種類のことをカインドと言ったりします。

カインドは REPL で :k を使用することで調べることができます。

> :k Int
Int :: *

> :k Maybe
Maybe :: * -> *

> :k Maybe Int
Maybe Int :: *

> :k Either
Either :: * -> * -> *

> :k Either Int
Either Int :: * -> *

> :k Either Int String
Either Int String :: *

この出力結果から以下のようなことが分かります。

  • 具体型は * で表現される(Int)。
  • 型変数を1つ受け取る型コンストラクタは、関数のように * -> * と表現される(Maybe)。
  • 1つ埋めると具体型 * になる(Maybe Int)。
  • 型変数が2つ以上のときも同様。

記号だらけで最初は戸惑うかもしれませんが、以下のような型専用の関数があると考えるとイメージしやすいかもしれません。

-- `* -> *` に対応
createMaybeType  :: Type -> Type
createMaybeType a = Maybe a

-- `* -> * -> *` に対応
createEitherType :: Type -> Type -> Type
createEitherType a b = Either a b

上記では Type となっている部分が、カインドでは単なる * という記号に置き換わるということですね。

以下の表は対応関係をまとめたものです。

Swift Haskell カインド
Int Int *
Optional Maybe * -> *
Optional<Int> Maybe Int *
Result Either * -> * -> *
Result<Int, AppError> Either Int AppError *

Swift との用語比較

さて、ここまで見てくると Haskell の「値コンストラクタ」や「型コンストラクタ」、「型変数」、「カインド」といったものは、用語こそ独特であるものの Swift におけるジェネリックスとほぼ同様のものであることが分かります。

Haskell と Swift とで用語の対応関係を表にしてみます(人によって呼び名が異なることも多いですが、イメージするのには役立つでしょう)。

Haskell Swift
値コンストラクタ コンストラクタ
型コンストラクタ ジェネリック型
型変数 ジェネリックパラメータ
カインド (一般的な呼称なし)

上記を踏まえて、もう一度 Maybe 型の定義を読んでみます。

data Maybe a = Nothing | Just a
  • Maybe a 型は、値コンストラクタ Nothing または値コンストラクタ Just a から生成できる。
  • Maybe は型コンストラクタで、a という型変数を受け取って具体型になれる。
  • Maybe* -> * 、すなわち1つの具体型を受け取ることで具体型になるカインドという型の種類を持つ。
  • Maybe Int といったように型を埋めることで、カインドは具体型 * になる。

なお、表にも記載したとおりカインドに相当する Swift の一般的な呼称はおそらく無いかと思います。これは Swift でプログラミングする際にカインドに相当するものを扱う必要がないためです(コンパイラは内部的に見ていると思いますが)。

もちろん、ここまで見てきたように型パラメータを埋めることで具体型として扱えるというのは考えるかもしれませんが、「型パラメータを1つ持つ Optional<T> と配列 [T] が同じ型の種類(* -> *)である」という事実をプログラミングに利用したことは無いはずです。

では、Haskell ではどのようなときに利用するのかと言うと、高カインド多相と呼ばれるものを扱うときに必要になってきます。これは Swift を含め、一般的に関数型言語と呼ばれる主流のプログラミング言語でも殆どサポートされていない機能です(Scala はサポートしているようです)。

高カインド多相については、またあらためて解説したいと思いますが、型にも種類があってそれを Haskell ではカインドと呼ぶことを頭の隅で覚えておくと良いでしょう。

まとめ

さて、復習も含めたため今回は長くなってしまいましたがまとめです。

  • Haskell には直積と直和のデータ型があり、どちらも data キーワードから作れる。
  • data {新しい型名} = {値コンストラクタ1} {フィールド1} … | {値コンストラクタ2} … といった構文。
  • 型変数がある場合は左辺が data {新しい型名} {型変数1} {型変数2} … といった形式になる。
  • どちらも パターンマッチ を利用して値を取り出したり分岐する。
  • レコード構文は直積型のためのシンタックスシュガー。
  • data Point = Point { x :: Int, y :: Int } のように記述する。
  • 既存の型にエイリアス(別名)を付けたいときは type キーワードを使う。
  • 既存の型をラップした新しい型をつくりたい場合は newtype キーワードを使う。
  • 値を生成するから 値コンストラクタ で、実体は単なる関数。
  • 型を生成するから 型コンストラクタ で、型専用の関数みたいなもの。
  • 型にも種類があって「カインド」と呼び、 * -> * などと表記する。
  • * が具体型。
  • 具体型でない * -> * などは、型変数を具体型で埋めて具体型にする必要がある。

今回は、新しい用語が多く登場してなかなか大変だったかもしれません。しかし、Swift との対応関係を考えると、それほどややこしい考え方は無かったのではないでしょうか。

現時点だと「カインド」は学術的興味を満たす以上のものではないと感じるかもしれませんが、Haskell の根幹を支える型クラス、そしてモナドにも繋がる非常に重要な概念なので、頭の片隅においておくと良いかもしれません。

さて、次回は Swift におけるプロトコルにあたる型クラスなどについて解説していく予定です。

あとがき

トウカイテイオーちゃんが好きになってきましたよぅ。(アニメ11話を視聴した系の話)

Written by tobi462.