直和型、タグ付きユニオン、代数的データ型

概要

直和型とタグ付きユニオン、代数的データ型について勉強した記録です。

そもそもこれらの用語に聞きなじみもいない方もいらっしゃるかと思います。
これらは現在のモダン言語にも少しずつ取り入れられてきましたが、既存の複数の型を組み合わせ一つの型を作成、使用できるものです。

一番身近でわかりやすい例で言いますと、 TypeScript のユニオン型が存在します。

let value: string | number;
 
value = "text";
value = 200;
 
if(typeof value === "string") { ... }
else if(typeof value === "number") { ... }

これはどちらか一方の型しか変数には入りません基本的に変数には一つの値しか入りませんから当たり前かと思います。
ただし、 TypeScript のユニオンは、string | stringのとき、その値が、どちらのstringであるかを明示的に判定できません。これは直和型とは厳密には異なります。因みにユニオン型は非交差和(untagged union)です。
ユニオン型は直和型と非常に近い概念ではありますが、厳密には識別子で明確に、どの型なのかを明確に区別するものを、直和型あるいはタグ付きユニオンといいます。

Rust では、最も実用的な形で実装されています。
Option型Result型です。
Rustですと、Option型はSome型とNone型の直和で構成されています。
これは値がある時とない時が存在するときに使用します。
値があるときは、Someでその値をラッパーして返す、ないならNoneを返す、その関数の帰り値はOption型になります。

pub enum Option<T> {
    None,
    Some(T),
}

これを使用するときは、

fn get_value(flg: bool) -> Option<i32> {
	if(flg) {
		return Some(200);
	}
	return None;
}

という風に書いて、これをOptionで返すことができます。

因みに TypeScript で直和型を実装しようとするなら、

type Success = {
  status: "success";
  data: string;
};
 
type Failure = {
  status: "error";
  message: string;
};
 
type Result = Success | Failure;

のように書くことができ、このときのResult型が直和型となります。
この型らではstatusが識別子の役割をします。

function handleResult(result: Result) {
  if (result.status === "success") {
    console.log(result.data);
  } else {
    console.error(result.message);
  }
}

そして直和型が実装されている言語にて、最も利用する機能としてパターンマッチングが存在します。
#lang/Rust なら

let result = Some(100);
match result {
	Some(v) => println!("value is {}", v),
	None => println!("value is empty"),
}

となります。

ちなみに TypeScript ではパターンマッチングが実装されていません。
個人的にパターンマッチングを最近よく使っているので、これだけの理由で TypeScript をちょっとだけ嫌いになりつつあります。
一応、現在だとType Narrowingがパターンマッチングとして機能しているようです。
書き方としてはこんな感じ、

if (result.status === "success") {
	
} else {
	...
}

直和型を作るもっともよい方法

直和型を作るにあたって最も良いというか、択一の方法として、sealedクラスを使う方法があります。
#lang/Kotlin なら

sealed class Result
class Success: Result()
class Failure: Result()
 
val result = Success()
 
when(result) {
	is Success ->
	is Failure ->
}

のように書くことができます。 Kotlin はパターンマッチングがwhenで書かれますね。

Dart なら freezed を使って

part 'result.freezed.dart';
 
@freezed
sealed class Result<T> with _$Result<T> {
  const Result._();
 
  const factory Result.success({required T value}) = Success<T>;
  const factory Result.failure({required Exceptions exceptions}) = Failure<T>;
}

というように書くことができます。パターンマッチングは、

switch (result) {
	case Success(): {
	
	}            
	case Failure(exceptions: final e):
	
	}
}

と書くことができます。
もちろん、 Dart でもsealedクラスは実装されているので、それを使用することもできます。

直積型と代数的データ型

直和型に相対する概念として、直積型が存在します。直和型がどちらか一方しか型を保持できないとしたら、直積型はどちらの値または型も保持できるものを差します。一般的には構造体であったり、クラスそのものであったり、タプルであったりを直積型といいます。

これらの概念を組み合わせて、複雑なデータ構造を作成するので代数的ということができます。これらを代数的データ型という場合もあります。このような用語が出てきたら、これを思い出してみてください。