ペンギン村 Tech Blog

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

3. データ型の基本 :: Swift プログラマのための Haskell 入門

さて、前回は関数の書き方やカリー化・部分適用などについて見てきました。

今回はデータ型の基本について見ていきたいと思います。

データ型の定義

Swift では、直積型を構造体、直和型を enum で表現できます。データ構造という観点から見ると、この2つの概念があれば基本的にどんなデータでも表現できるでしょう。

Haskell では data キーワードで、直積型および直和型を定義できます。

直積型の定義

以下は Swift で 文字列数値 を組み合わた Person 型を定義したものです。

struct Person {
    let name: String
    let age: Int
}

Haskell では以下のようになります。

data Person = Person String Int

直積はデータの組み合わせを表現したものと考えることができます1

声に出して読むと「 StringInt を持つ型 Person を定義する」といったところでしょうか。コード中で Person が2回登場しているのを不思議に思うかもしれませんが、現時点では考えなくても大丈夫です。

さて、ここで疑問に思うのは「Haskell ではプロパティ名をつけることができないのだろうか?」ということでしょう。実はレコード構文と呼ばれるものが用意されており、構造体と似たような定義も可能です。

data Person = Person
    { name :: String
    , age  :: Int
    }

レコード構文については後日取り上げたいと思うのですが、これは一種のシンタックスシュガーのようなもので、データ構造に注目した場合はどちらも変わらないということだけ抑えておきましょう。

そういった意味では、Swift においてタプルに対して typealias で別名をつけたものと考えたほうが分かりやすいかもしれません。

typealias Foo = (Int, String, Bool)
typealias Foo = (number: Int, string: String, bool: Bool) // ラベルをつけることも出来る

直和型の定義

Swift では enum を利用して直和のデータ構造を表現できます。

以下は green or yellow or red のいずれかの値を持つ Color 型を宣言したものです。

enum Color {
    case green
    case yellow
    case red
}

Haskell では直積と同じ data キーワードを使用して定義します。

data Color = Green
           | Yellow
           | Red

以下のように1行で書くことも出来ます。

data Color = Green | Yellow | Red

直和はいずれかのデータを表現したものと考えることができます2

論理和の記号である | を使用して区切られているので、「データ型 ColorGreen または Yellow または Red である」と自然に読めるかと思います。

なお、Swift では小文字であるのに対して、Haskell では大文字になっていることにお気づきでしょうか。実は Haskell では「大文字始まり」と「小文字始まり」が区別される場面があり、型にまつわるものは全て大文字始まりでなければなりません。

関連値を持つ直和型

ところで Swift では enum で関連値を持つことができます。みんなが大好きな Optional は次のように定義できます。

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

「Optional 型は、任意の型 T の値を持つ some または値を持たない none である」という定義によって、値が入っているかもしれないというデータ型を表現しているわけですね。

Haskell では以下のように定義できます。

data Optional t = Some t
                | None

構文がシンプルなので最初は戸惑うかもしれませんが、両者を比較すれば実質的に同じものを定義しているだけであることがわかります。

Swift ではジェネリック型を表現する T型パラメータと呼んだりしますが、Haskell のコードで登場している t型変数などと呼ばれます3。どちらも型を構成する一部の型が確定しておらず後から型を埋めるので「パラメータ」や「変数」といった表現をするのですね(たぶん)。

ちなみに、ここでは Swift のコードに対応させるように Optional という名前で定義しましたが、実際の Haskell では Swift の Optional に相当するものとして Maybe 型が標準で定義されています。

data Maybe a = Just a
             | Nothing

ちょうど a という値(Just a)か、存在しない(Nothing)ということですね、たぶん(Maybe)。

値の生成

さて、せっかくデータ型を定義する方法を学んだのですから、実際に値を生成したいものです。

例として、以下の Swift コードの型を見ていきます。

enum Gender {
    case male
    case female
}

struct Person {
    var name: String
    var gender: Gender
}

性別である Gender 型と、名前と性別を持つ Person 型を宣言しています。

Haskell では次のように定義できます。

data Gender = Male | Female
data Person = Person String Gende

値の生成

さて、Swift では以下のように値を生成しますが、

// Swift
let person: Person = Person(name: "tobi462", gender: .male)

Haskell では以下のように生成します。

-- Haskell
person :: Person
person = Person "tobi462" Male

Haskell は関数適用と同じようにスペースで要素を区切って Person "tobi462" Male として値を生成するのが分かります。

一般的なプログラミング言語で「コンストラクタ」と呼ばれるように、Haskell でもこの Person のことを値コンストラクタと呼んだりするのですが、その実態は単なる関数となっています。

実際にそうなのかは GHCI で :t を使用して Person の型を確認してみると分かります。

> :t Person
Person :: String -> Gender -> Person

つまり、Person というのは StringGender を受け取って Person 型を返す(値コンストラクタという特別な名前を持つ)関数なのですね。

前回の記事で見てきた add 関数と比較すると分かりやすいでしょう。通常の関数と同じようにカリー化されているのも確認できます。

add :: Int -> Int -> Int
add x y = ...

Person :: String -> Gender -> Person
Person = ...

-- 関数呼び出し
add 1 2
Person "tobi462" Male

-- ()で優先順位を明示すると
((add 1) 2)
((Person "tobi462") Male)

ちなみに、コンストラクタが関数であるのが特別なことのように書きましたが、実は Swift のコンストラクタも .init で参照を取得すれば単なる関数と同じように扱えます。

let constructor = Person.init
constructor("tobi462", .male)

再帰的?

ところで、以下の定義を読んで Person が再帰的に定義されているように感じた方もいるかも知れません。

Person :: String -> Gender -> Person

しかし、Swift のコード例から明白なようにデータ構造が再帰的であるわけではありません。これはどういうことでしょうか?

詳細や用語についてはあらためて解説したいと思うのですが、Haskell では型名とコンストラクタ名が区別されるようになっています。実は先ほどの例では、たまたま同じ名前であったため再帰的に見えましたが、両者は別物なのです。

すなわち以下のような定義も可能ということです。

data Person = InitPerson String Gender

person :: Person
person = InitPerson "tobi462" Male

言葉にすると「データ型 Person は、StringGender を受け取る値コンストラクタ InitPerson によって値を生成できる」ということです。

パターンマッチ

さて値の生成はできましたが、値を判別したり値を取り出すにはどうするのでしょうか?

例として、男性だった場合に Hi, {name} 、女性だった場合に Hello, {name} という文字列を返す関数を実装してみたいと思います。

Swift では次のようになるでしょう。

func hello(_ person: Person) -> String {
    switch person.gender {
    case .male:
        return "Hi, " + person.name
    case .female:
        return "Hello, " + person.name
    }
}

Swift では プロパティ で直積型から値を取り出し、switch で直和型の場合分けをする事ができるのでした。

一方、Haskell ではどちらもパターンマッチによって値を取り出したり型の場合分けをします。

hello :: Person -> String
hello person = 
    case person of
        (Person name gender) -> case gender of
            Male   -> "Hi, " ++ name
            Female -> "Hello, " ++ name

なんだか急に複雑になりましたね(これは説明のために最も愚直な書き方をしているだけなので、どうかこれで Haskell を嫌いにならないでください!)。

でも Swift のコードと対応させれば、何を意味しているのかはなんとなく分かるかと思います。

case-of

何が起こっているのか正しく理解するために、case-of 式 の使い方を見ていきたいと思います。

case-of 式は Swift における switch 文と非常によく似ています。直和型のところで登場した Color を使ってコードを比較してみましょう。

まずは Swift のコードからです。

func signal(_ color: Color) -> String {
    switch color {
    case .green:  return "Go!"
    case .yellow: return "Caution!"
    case .red:    return "Stop!"
    }
}

Haskell では次のようになります。

signal :: Color -> String
signal color =
    case color of 
        Green  -> "Go!"
        Yellow -> "Caution!"
        Red    -> "Stop!"

いくつか違いが読み取れます。

  • switch 変数 {} の代わりに case 変数 of という構文。
  • case パターン: の代わりに パターン -> でマッチさせる。
  • case-of は式なので値を返せる。

構文が異なるのを除けば、Haskell の case-of は式なので値を返せるというのが大きな違いであると分かります。

では、関連値を持つ直和型の場合はどういう書き方になるでしょうか?Optional 型で違いを見てみます。

switch optionalValue {
case .some(let value): return "Found " + value
case .nothing:         return "Not found..." 
}

Haskell では次のようになります。

case optionalValue of
    Some value -> "Found " ++ value
    None       -> "Not found..."

パターンマッチの書き方が異なるのと let などのキーワードが不要な点を除けば、基本的に同じ構造をしているのが分かります。

値が不要な場合はどちらも _ で捨てることが出来ます。

// Swift
switch optionalValue {
case .some(_): return "Found!"
case .nothing: return "Not found..." 
}
-- Haskell
case optionalValue of
    Some _ -> "Found!"
    None   -> "Not found..."

さて、ここまで見てくると先ほどのコードの意味も理解できるのではないでしょうか?

hello :: Person -> String
hello person = 
    case person of
        (Person name gender) -> case gender of
            Male   -> "Hi, " ++ name
            Female -> "Hello, " ++ name

まず、3行目の case person ofperson を判定対象にして、4行目の (Person name gender) にパターンマッチさせています。先ほどの例で Some value で中身を取り出したのと同じように、namegender をパターンマッチで取り出しています(これは値の分解とも表現できるかもしれません)。パターンが1行しか書かれていませんが、これは Person 型の値が Person 1つしか存在しないためですね。

次に同じく4行目で、取り出した値 gendercase gender of にて判定して、さらなるパターンマッチを行っています。その後、 MaleFemale で場合分けしてそれぞれの処理を記述しています。

Swift のコードで、Person を構造体ではなくタプルのエイリアスとして表現すると、より対応関係が分かりやすいかもしれません。

func hello(person: (String, Gender)) -> String {
    switch person {
    case (let name, let gender):
        switch gender {
        case .male:   return "Hi, " + person.name
        case .female: return "Hello, " + person.name
        }
    }
}

スマートな書き方

さて、先ほどの Haskell コードを読むと、なんだか Haskell はインデントレベルが簡単に深くなって読みづらくなってしまうように感じてしまいます。しかし、Haskell には便利なシンタックスシュガーが用意されています。いくつか例を見ていきましょう。

まず、case-of を使わず、関数の引数部分でパターンマッチを記述することができます。

hello :: Person -> String
hello (Person name gender) = 
    case gender of
        Male   -> "Hi, " ++ name
        Female -> "Hello, " ++ name

これだけでも大分スッキリしましたね?

さらにパターンマッチは入れ子になっていても機能します。つまり、Person のパターンマッチにて Gender も同時にパターンマッチすることが出来ます。

hello :: Person -> String
hello (Person name Male)   = "Hi, " ++ name
hello (Person name Female) = "Hello, " ++ name

関数が複数定義されているように見えて最初はややこしいですが、Haskell ではパターンマッチを非常に多用するため、これは非常に一般的な書き方となっています。

最後におまけとして、前回で覚えた wherelet-in も使ってみましょう。

-- `where` を使った例
hello :: Person -> String
hello (Person name gender) = prefix ++ name
    where
        prefix = case gender of
            Male   -> "Hi, "
            Female -> "Hello, "
-- `let-in` を使った例
hello :: Person -> String
hello (Person name gender) = 
    let prefix = case gender of
                     Male   -> "Hi, "
                     Female -> "Hello, "
    in prefix ++ name

どう読んだらいいの?

case-of 式の書き方を見て、何だか読みやすいような読みづらいような・・・と感じた方もいるかもしれません。個人的にどのように読むと分かりやすいのかを、ここで書いてみたいと思います。

まず基本形は以下のとおりで、上から順にパターンに合致するか判定が行われていきます。

case 変数 of
    パターン1 -> マッチした場合の処理
    パターン2 -> 〃
    パターンn ->

そして重要なのが「パターン」の書き方となるわけですが、(部分的に変数が利用可能な)値そのものを記述していると考えると分かりやすいかと思います。

どういうことかと思うでしょうが、データ定義値の生成パターンマッチを並べてみると言いたいことが分かるかと思います。

-- データ定義
data Person = Person String Gender

-- 値の生成
Person "tobi462" Male

-- パターンマッチ
case person of
    (Person "tobi462" Male)   -> ...
    (Person name      Male)   -> ...
    (Person name      Female) -> ...

このように並べると、どれも Person name gender という書き方で統一されていることが分かります。そしてパターンマッチの1行目は Person "tobi462" Male と値の定義と寸分も変わりません。このような視点で見ると、パターンは部分的に変数が使える単なる値の定義である という読み方もできるかもしれません。

ちなみに、このように書き方が一致しているケースは他にもあって、関数の定義と適用も同じ構文になっていることが分かります。

add :: Int -> Int -> Int
add x y = x + y

add 1 2 -- => 3

変数 x と変数 y に適用する場合は add x y となり、関数の定義と一致することが分かるでしょう(関数の書き方によっては一致しないことも多いので、あくまで1つの考え方としておくとよいでしょう)。

まとめ

さて、今回のまとめです。

  • Haskell にはデータ構造として直積直和が存在する。
  • どちらも data キーワードを使用して定義する。
  • 直積はタプルのようなもので data Person = Person String Gender のように定義する。
  • 直和は enum のようなもので data Color = Green | Yellow | Red のように定義する。
  • 値の生成はPerson “tobi462” Male のように行える。
  • 値コンストラクタ Person は関数 String -> Gender -> Person であると見なせる。
  • 値コンストラクタも通常の関数と同じようにカリー化されている。
  • case-of 式によるパターンマッチで値の判定や取り出しが行える。
  • 関数の引数部分でもパターンマッチでき、その場合は関数の定義が複数になることもある。

やや詰め込みすぎだったでしょうか?

レコード構文や再帰的データ型など、この記事で説明しきれなかった内容については、後日あらためて取り上げたいと思っています。

さて、次回は Swift における配列に相当するリストを題材として、ラムダ式や関数合成などについて見ていく予定です(また詰め込むつもりでしょうか・・・)。

blog.penginmura.tech

あとがき

プロギアの嵐とかケツイみたいな変則的な弾の動きをする弾幕 STG は苦手なんですよぉ・・・(普通の弾幕 STG が得意だとは言ってない

Written by tobi462.


  1. 各要素が取りうる状態の数の が、取りうる状態の数となるので直積なのですね(たぶん)。

  2. 各要素が取りうる状態の数のが、取りうる状態の数となるので直和なのですね(たぶん)。

  3. 型変数は小文字始まりでなければなりません。