さて、前回は関数の書き方やカリー化・部分適用などについて見てきました。
今回はデータ型の基本について見ていきたいと思います。
データ型の定義
Swift では、直積型を構造体、直和型を enum で表現できます。データ構造という観点から見ると、この2つの概念があれば基本的にどんなデータでも表現できるでしょう。
Haskell では data
キーワードで、直積型および直和型を定義できます。
直積型の定義
以下は Swift で 文字列
と数値
を組み合わた Person
型を定義したものです。
struct Person { let name: String let age: Int }
Haskell では以下のようになります。
data Person = Person String Int
直積はデータの組み合わせを表現したものと考えることができます1。
声に出して読むと「 String
と Int
を持つ型 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。
論理和の記号である |
を使用して区切られているので、「データ型 Color
は Green
または 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
というのは String
と Gender
を受け取って 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
は、String
と Gender
を受け取る値コンストラクタ 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 of
で person
を判定対象にして、4行目の (Person name gender)
にパターンマッチさせています。先ほどの例で Some value
で中身を取り出したのと同じように、name
と gender
をパターンマッチで取り出しています(これは値の分解とも表現できるかもしれません)。パターンが1行しか書かれていませんが、これは Person 型の値が Person
1つしか存在しないためですね。
次に同じく4行目で、取り出した値 gender
を case gender of
にて判定して、さらなるパターンマッチを行っています。その後、 Male
と Female
で場合分けしてそれぞれの処理を記述しています。
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 ではパターンマッチを非常に多用するため、これは非常に一般的な書き方となっています。
最後におまけとして、前回で覚えた where
や let-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 における配列に相当するリストを題材として、ラムダ式や関数合成などについて見ていく予定です(また詰め込むつもりでしょうか・・・)。
あとがき
プロギアの嵐とかケツイみたいな変則的な弾の動きをする弾幕 STG は苦手なんですよぉ・・・(普通の弾幕 STG が得意だとは言ってない
Written by tobi462.