🪨

代数的データ型がめっちゃええ。

2023/12/19に公開

本記事はUzabase Advent Calendar 2023の19日目の記事です。

https://qiita.com/advent-calendar/2023/uzabase

はじめに

株式会社ユーザベース 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つのパターンが存在します。

  1. 電話番号もメールアドレスも持っている
  2. 電話番号は持っているが、メールアドレスは持っていない
  3. 電話番号は持っていないが、メールアドレスは持っている
  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

もしもパターンが網羅されていない場合は、エディターや実行時に警告が表示されます。

そのため、バグが混入するリスクを減らすことができます。

このように代数的データ型とパターンマッチングを組み合わせることで、とても簡潔に型安全な実装を行うことができます。

おわりに

今回は「代数的データ型、めっちゃええねん」ということをお伝えしてきました。

代数的データ型は不正な状態を表現できないようにしやすいため、堅牢なシステムを構築するのにとても有用です。

積極的に活用して、みんながハッピーなシステムを作っていきましょう!

脚注
  1. Product Teamで3ヶ月に一度開催される、所属するチームを変更する取り組み ↩︎

Discussion