代数的データ型がめっちゃええ。
本記事はUzabase Advent Calendar 2023の19日目の記事です。
はじめに
株式会社ユーザベース BtoB SaaS Product Team(以下 Product Team)の山室です。
チームシャッフル[1]でこの10月から新しい開発チームに移籍したのですが、そこで初めて関数型プログラミング言語であるF#に触れました。
F#について学んでいく中で「代数的データ型」という概念を知り、普段の開発で使っていくほどに「めっちゃええやん」だったので、今回は代数的データ型についてまとめたいと思います!
代数的データ型 is 何?
代数的データ型(Algebraic Data Type: ADT)とは、特に関数型プログラミングで使われる、型を組み合わせることによって作られる型で、次の2種類から構成されます。
直積型
直積型(Product Types)は「積(AND)」を表現するデータ型で、複数の型を同時に保持する型です。
F#では、レコード(Record)やタプル(Tuple)などで表現されます。
// Record
type Person =
{ Name: string
Age: int
Address: string }
// Tuple
type Point = int * int
オブジェクト指向言語におけるクラス(Class)も直積型に分類されます。
直和型
直和型(Sum Types)は「和(OR)」を表現するデータ型で、複数の型のうちのどれかひとつを表す型です。
F#では、判別共用体(Discriminated Union)として表現されます。
type Color =
| Red
| Blue
| Green
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
何が嬉しいの?
代数的データ型がどのようなものかが分かったところで、それでは一体、代数的データ型があることによって何が嬉しいのでしょうか?
それは 「不正な状態を表現できないようにできる(しやすい)」 ということです。
不正な状態を表現できないようにできる(しやすい)
具体的な例を用いて説明します。
例えば、次のようなユーザーを表現するレコードがあるとします。
type User =
{ Name: string
PhoneNumber: string option
MailAddress: string option }
このデータ構造では、次の4つのパターンが存在します。
- 電話番号もメールアドレスも持っている
- 電話番号は持っているが、メールアドレスは持っていない
- 電話番号は持っていないが、メールアドレスは持っている
- 電話番号もメールアドレスも持っていない
ここで、次のようなビジネスルールを表現したい場合、
ユーザーは電話番号もしくはメールアドレスのどちらかは必ず持っていなければならない
4つ目のパターンはビジネスルールから逸脱することになり、結果として不正なデータを生み出すことに繋がってしまう可能性があります。
こうした状況を、代数的データ型を活用することによって解消することができます。
具体的には、次のように判別共用体を用いて電話番号とメールアドレスをコンタクトとして抽出し、ユーザーはそのコンタクトの型を持つようにします。
type Contact =
| PhoneNumber of string
| MailAddress of string
| PhoneNumberAndMailAddress of string * string
type User =
{ Name: string
Contact: Contact }
こうすることで、「電話番号もメールアドレスも持っていない」というパターンはなくなり、不正な状態を表現することができなくなります。
パターンマッチング(Pattern Matching)
また、代数的データ型のもうひとつのメリットとして、パターンマッチングとの相性の良さも挙げられます。
先程のコンタクトに対してパターンマッチングを適用した例が以下になります。
let output (contact: Contact) =
match contact with
| PhoneNumber(phoneNumber) -> printfn "PhoneNumber: %s" phoneNumber
| MailAddress(mailAddress) -> printfn "MailAddress: %s" mailAddress
| PhoneNumberAndMailAddress(phoneNumber, mailAddress) ->
printfn "PhoneNumber: %s, MailAddress: %s" phoneNumber mailAddress
もしもパターンが網羅されていない場合は、エディターや実行時に警告が表示されます。
そのため、バグが混入するリスクを減らすことができます。
このように代数的データ型とパターンマッチングを組み合わせることで、とても簡潔に型安全な実装を行うことができます。
おわりに
今回は「代数的データ型、めっちゃええねん」ということをお伝えしてきました。
代数的データ型は不正な状態を表現できないようにしやすいため、堅牢なシステムを構築するのにとても有用です。
積極的に活用して、みんながハッピーなシステムを作っていきましょう!
-
Product Teamで3ヶ月に一度開催される、所属するチームを変更する取り組み ↩︎
Discussion