代数的データ型の作り方(値コンストラクタ,型コンストラクタについて)

2022/07/14に公開

この記事では代数的データ型の作り方について学びます。代数的データ型とは新しいデータ型を作る仕組みであり、Haskellには3種類の作り方(AND/OR/再帰)があります。 カンタンにいえばオブジェクト指向でいう「クラス」に相当する仕組みです。「新しいデータ型を作る」という点は同じですが、どのような機能が追加されているかに注目して下さい。(1)集合としてのデータ型、(2)クラスとは何か? という2つのトピックをおさらいしてから代数的データ型の内容に入っていきます。

(1)集合としてのデータ型

そもそもデータ型とは扱うデータの種類・大きさを指定する仕組みです。なぜデータの種類・大きさを指定するかと言えば、コンピュータへ事前にその情報を知らせるためです。「これくらいの大きさのデータを扱いますよ」とコンピュータに伝えることで、メモリを効率的に使用できます。また間違ったデータ同士で計算するのを防いだり、プログラムが読み書きしやすくもなります。

物理的/論理的なデータ型の形

データ型はその捉え方によって以下のように考えることができます。物理的にはメモリICであり、論理的にはビルディング/集合ともいえます。

データ型の形 捉え方
物理的な形 メモリIC
論理的な扱い ビルディング
数学っぽい捉え方 集合

データ型の正体はメモリICです。メモリICにあるピンの1本1本が電気のON/OFFをします。これによってデータを格納する場所(アドレス)を表します。

しかしメモリICのピン(0と1の数字)としてメモリを扱うのは少し考えづらいですよね。そこで以下のようなビルディングとして捉えることにしました。

メモリが実際にこのようなビルディングの形ではない点に注意して下さい。あくまで実体としてはメモリICのピンです。しかし私たちが扱いやすいように捉え方を変えています。論理的というのは「(実際の形とは違うけれども)私たちが扱いやすい/考えやすい」という意味です。この中でデータ型はどのように扱われるのでしょうか。例えばIntというデータ型があった際、4フロア分のメモリスペースが確保されます。0と1の数字を並べていただけの時よりもこちらの方がわかりやすいですよね。

集合としてのデータ型

しかしビルディングだけでもまだ分かりづらいです。どのような性質のデータが入るのかがわからないからです。 4フロアを占領することがわかっても、整数で10などが入る..といった、私たちがコードを書く上で最も大切な情報は分かりづらいです。そこでデータ型を集合として考えることにしました。例えばIntというデータ型は-2147483648~2147483647までの範囲の中から1つの値を取ります。-2147483648~2147483647までの集合があって、その中から1つの値を取るモノと考えることができます。

先ほどのビルディングよりも「どんな性質のデータが入るのか?」が直感的に理解しやすいですよね。物理的な形はメモリICであることに間違いないですが、論理的にいえばデータ型はビルディング、更には集合として考えることができます。代数的データ型にもOR条件というのがあって、データ型を集合として捉えることが理解の助けとなるので、ここまでくどくど説明させて頂きました。

(C++ですがこんな記事もあったので参考にすると良いかもしれません。)

https://docs.microsoft.com/ja-jp/cpp/cpp/data-type-ranges?view=msvc-170

(2)そもそもクラスとは?

まずはオブジェクト指向のクラスから考えてみます。クラスとは変数/関数をまとめて扱うための仕組みです。 プログラムを作っていると複数のデータをまとめて扱いたい場面があります。例えば社員の情報を管理する場面です。社員の名前だけでなく、社員番号/役職/年齢..など色々な情報が集まって意味をなしますよね。そんな場合にデータ型で関連するデータをまとめることができます。C++でEmployee(社員)クラスを作ったのが以下です。

#include <iostream>
using namespace std;

class Employee{
    public:
        int num;
        int year;
};

int main (){
    Employee Tanaka;
    Tanaka.num = 10;
    cout << Tanakanum << "\n";
    return 0;
}
// 実行結果「10」

Employeeというクラスでnum,yearという2つの変数を管理しています。(今回はHaskellと比べるため関数は含めませんでした。)クラスを作ることで必要な情報をまとめて効率的に扱うことができます。

なぜクラスは必要なの?

クラスはなぜ必要なのでしょうか。なぜならプログラムで用意されているのは単独のデータ型だけだからです。int/char/doubleなどは1つのデータしか扱うことができません。それだと複数の情報を関連させる時に不便ですよね。扱うデータが1つだけでは足りない、複数のデータをまとめて扱いたい場面んでクラスは必要となります。

代数的データ型とは?

Haskellの代数的データ型はオブジェクト指向のクラスに相当する仕組みです。代数的データ型とは新しいデータ型を作る仕組みです。 Employeeというクラスでnum,yearという2つの変数を管理する例がありましたが、Haskellでも同じように実現できます。

data Employee = Tanaka Int Int deriving Show
tanaka :: Employee
tanaka = Tanaka 10 34
main = print tanaka
-- 実行結果「Tanaka 10 34」

既存のデータ型num,yearをまとめて、新しくEmployeeというデータ型を作りました。このようにオブジェクト指向でいうクラスと同じようなことができます。

代数的データ型には3種類

代数的データ型はデータ型をまとめるための仕組みですが、ただまとめるだけではありません。OR条件で持つべき値を指定したり、再帰として自分自身のデータ型を指し示すように指定できます。Haskellの代数的データ型には以下の3種類があります。(AND/OR/再帰という名前は正式な名前でないので悪しからず..。)

代数的データ型 機能
OR OR条件で持つべき値を指定
AND データ型をまとめて新しいモノを作る
再帰 自身と同じデータ型を指し示す

(3)代数的データ型の定義

代数的データ型の定義は以下です。dataと書いた後に新しい型名を宣言します。型コンストラクタが新しい型名、新しいデータ型で取り得る値が値コンストラクタとなります。()内はあってもなくても構いません。)

data 型コンストラクタ (※型引数) = 値コンストラクタ (※既存のデータ型 or 型引数)

なんだか難しそうですが実際に作ってみるのが1番です。OR型の代数的データ型から解説していきます。具体例としてColorという新しいデータ型を作ってみます。Color型が取り得る値としてRed,Blue,Yellowという値があったとします。3つの値を持ち得るColor型は以下のように定義できます。

data Color = Red | Blue | Yellow

データ型を集合として捉えられることを思い出して下さい。代数的データ型を理解するにはIntと比べると理解が早いです。Intは-2147483648 ~ 2147483647の範囲にある整数から構成されていました。それを代数的データ型っぽく定義するとこのようになります。

data Int = -2147483648 | -2147483647 | .. | -100 | .. | 0 | 1 | 2 | .. | 10 | .. | 2147483646 | 2147483647

(値は多いですが)Color型とそっくりな定義になりました。Color型もInt型もこの値の範囲の中から1つの値を取ることになります。

Color型:Red,Blue,Yellow の中から1つの値を取る
Int型:=-2147483648 ~ 2147483647 の中から1つの値を取る

集合としての値の中から1つの値をとるというのが、代数的データ型を理解するためのポイントとなります。

(4)コンストラクタとは何か?

ここまでカンタンな例を使って代数的データ型の基本を学んできました。ここから更に応用編に入っていきます。代数的データ型にはコンストラクタという言葉が入っています。コンストラクタにはどのような役割があるのでしょうか。

data 型コンストラクタ (※型引数) = 値コンストラクタ (※既存のデータ型 or 型引数)

コンストラクタは日本語で構築子・構築するモノと訳されます。何かを作るモノと理解するのが良いかもしれません。そこに型,値という言葉が付いてますので、型を作るモノ・値を作るモノという意味があります。実質的には型を作るための型、値を作るための値..ということになりますが、すごく不思議な概念です。まるで機械を作るための機械みたいな..。順番に解説していきます。

値コンストラクタとは?

値コンストラクタはデータ型で取り得る値のことでした。生徒の情報を管理するStudent型を作ります。生徒は田中さん,佐藤さんの2人だったとします。代数的データ型で表現するとこのようになります。

data Student = Tanaka | Satou

図にすると以下のようになります。集合として表現すると非常に分かりやすいですよね。

値コンストラクタは値を構築するモノであり、引数を渡すこともできます。 生徒の番号を追加で管理するとします。すると引数としてデータ型を渡すこともできます。

data Student = Tanaka Int | Satou Int

Student型はTanaka Intという形をした値か、Satou Intという形をした値のどちらかを取ることになります。図にすると以下のようになります。

実際に引数を渡して出力にかけると次のようになります。田中さんは出席番号が1番だったとして、1という数値を引数として渡しました。

data Student = Tanaka Int | Satou Int deriving Show
tanaka :: Student
tanaka = Tanaka 1
main = print tanaka

-- 実行結果「Tanaka 1」

値コンストラクタは引数を取るので関数ともいえます。(1)データ型で取り得る値であること、(2)引数を渡して新たな値を作れること..という2つの機能があることを覚えておきましょう。値コンストラクタがコンストラクタである所以は(2)引数を渡して新たな値を作れることにあるといえます。

型コンストラクタとは?

型コンストラクタとは新しい型名を表していました。値コンストラクタにも引数を渡せることを学んできましが、データ型にも引数を渡すことができます。「データ型に対してデータ型を渡すことで新たなデータ型を作れる」という点で、データ型を作るモノ(=型コンストラクタ)というワケです。

ただしデータ型に引数を渡すには、型の定義で型変数aを使用することになります。型コンストラクタの後に型変数を付け加えることで、代数的データ型で多相型(任意のデータ型を受け取れる)を実現できます。 2つの座標軸を表すPoint型というデータ型を新しく作ってみます。x軸、y軸の値は整数かもしれませんし、小数かもしれません。IntかDoubelか決めかねる時には、後からデータ型を引数として渡して、受け取った引数を元にデータ型を作るようにします。

data Point a = Double a a deriving Show
p1 = Double 10 20
-- p1がPoint Int型だと判断される
main = print p1

Point a型に引数としてInt型を渡しました。これによってPiont Int型という新しいデータ型を作ることができました。他にも引数を受け取る型コンストラクタとして有名なのがMaybeです。Maybeは成功/失敗を表すためのデータ型ですが、色々なデータ型で利用できるようにしたいですよね。型は次のように定義されています。

data Maybe a = Just a | Nothing

Maybe a型となっており、任意のデータ型を受け取れるようになっています。Int,String,Doubleなどのデータ型を渡すことで、Maybe Int,Maybe String,Maybe Doubleなどのデータ型を作ることができます。

代数的データ型には他にも色々なトピックがありますが、その概要と値コンストラクタ,型コンストラクタの基本までまとめてみました。

Discussion