Elmに学ぶ集合としての型

2024/10/18に公開

話すこと

概要

プログラミングにおいて、型を集合として考える視点は非常に重要です。この記事では、Elmの例を通して型を集合として捉える方法について解説します。

Elmにおける型と集合

まず、ElmにおけるCustomTypeを取り上げ、型を集合として理解する方法を探ります。CustomTypeは複数の異なるケースを持つ型を定義する機能です。これにより、異なるデータの集合を一つの型として扱うことができます。

CustomTypeの例

Program.elm
type Program
    = Raymonda
    | LaSylphide Bool

このコードではProgramというCustomTypeが定義されています。この型にはRaymondaとLaSylphideの2つのバリアント※1があり、各バリアントは異なるデータを保持しています。

※1 バリアントとはカスタム型を構成する一つの値のことを指すと理解しています。

パターンマッチングによるCustomTypeの分解

Elmの強力な機能であるパターンマッチングを使えば、CustomTypeを分解して各バリアントにアクセスし関連するデータを取り出すことができます。

Program.elm
choreographer : Program -> String
choreographer program = case program of
    Raymonda -> "Marius Petipa"
    LaSylphide isDenmark -> 
        if isDenmark then "Auguste Bournonville"
        else "Filippo Taglioni"

この例では、Program型の各バリアントに応じて人名を返しています。

TypeAliasの例

また、type aliasを用いることで単一のバリアントに対して型を定義することも可能です。ただし、type aliasはCustomTypeの代替ではありません。

Program.elm
type alias LaSylphide =
    { isDenmark : Bool
    }

集合としての型

型を集合として捉えると、各型に「濃度」が存在することがわかります。濃度とは、集合に含まれる要素の数、すなわち型に対応するデータの数を指します。

Programの例ではRaymondaとLaSylphideという2つのバリアントがあり、それぞれ異なるデータの組み合わせを持っています。Raymondaはデータを持たないため濃度は1、一方、LaSylphideはBool型のデータを持ち、2つの組み合わせが可能です。したがって、Program型の濃度は合計で3です。

Programバリアント データ データ数
Raymonda - 1
LaSylphide true / false 2

濃度の乗算

型の濃度は乗算することができます。乗算はバリアントが保持するデータ間で行われます。以下の例を見てみましょう。

User.elm
type CustomUser = User Bool Bool

CustomUser型のUserバリアントは2つのBool値を持っています。各Bool値は2つの値(true/false)を取るため、CustomUserの濃度は4になります。

CustomUserバリアント データ データ数
User (true/false) (true/false) 4
first boolean second boolean
false false
false true
true false
true true

濃度の加算

型の濃度は、CustomTypeのバリアント間で加算できます※2。以下の例を考えてみましょう。

Color.elm
type Color
    = Red
    | Yellow
    | Blue

Color型は、Red、Yellow、Blueの3つのバリアントで構成され、それぞれデータを持たないため、Colorの濃度は3です。

Colorバリアント データ データ数
Red - 1
Yellow - 1
Blue - 1

※2 厳密には和の計算が行わえるのはCustomTypeの濃度だけではなく、TypeAliasやタプルでも和の計算が行われます。

濃度の意識の重要性

濃度を意識して型設計を行うことは、不要なパターンを排除し、コードをより明確にするために重要です。たとえば、ロード状態を表すデータ型を考えてみましょう。

LoadingState.elm
type LoadingState a
    = Loading
    | Loaded a

このLoadingState型の濃度は一番少なくて2です。

LoadingStateバリアント データ データ数
Loading - 1
Loaded a aのデータ数※3

次に、type aliasを使って近いものを表現してみます。

LoadingState.elm
type alias LoadingState a =
    { isLoading : Bool
    , data : Maybe a
    }

この場合、濃度は最低でも4になります。

isLoading data
false Nothing
false Just a
true Nothing
true Just a

このように、type aliasを使うと不要なデータの組み合わせが増え、処理の複雑さが増してしまいます。これは型の濃度を意識しなかった結果、濃度の乗算が行われているからです。

LoadingState.elm
display : (a -> String) -> LoadingState a -> String
display toString loadingState =
  case loadingState.data of
    Nothing ->
      if loadingState.isLoading then
        "Loading"
      else
        "Loaded ? Nothing ?"
    Just value -> 
      if loadingState.isLoading then
        "Loading ? Loaded ?"
      else
        String.join " " ["Loaded", toString value]

これに対し、CustomTypeを正しく設計することで、この問題を避けることができます。CustomTypeを使って再実装します。

LoadingState.elm
display : (a -> String) -> LoadingState a -> String
display toString loadingState =
  case Loading -> "Loading"
  case Loaded value -> String.join " " ["Loaded", toString value]

再実装後は分岐が2つになりました。また型の加算が扱えるようになったことで細かな型の濃度の調整も容易です。例えばロード状態に失敗の状態が増えた場合であってもCustomTypeにバリアントを一つ追加してあげれば終わりです。※4

LoadingState.elm
type LoadingState a
    = Loading
    | Loaded a
    | Failure

※3 Elmにおいて型定義に出てくる小文字は任意の型を表します。なのでこの表ではaのデータの数はわからないです。そのため数値ではなくaのデータ数と表記しています。

※4 だいぶと雑な例です。実際はLoadingStateに失敗のパターンを追加するのは意味的なところで違和感があるので、個人的には別の方法を取るのをお勧めします。

まとめ

この記事では、プログラミングにおける型を集合として捉える視点が、どのように型設計に影響を与えるかを探りました。特に、ElmのCustomTypeを例に、型が持つデータの組み合わせ(濃度)を理解することの重要性を解説しました。型の濃度を意識することで、無駄なパターンを排除し、シンプルで明確なコードを設計することが可能になります。

終わりに

TypeScriptもまた言語こそ異なりますがElmと同じフロントエンド領域における静的型付け言語です。
実際にElmで見かける型のテクニックはTypeScriptでも再現可能です。Elmから型のテクニックを学ぶのも良いのではないでしょうか。

Elmを通じて型を学んでみたいと思った方は是非ガイドラインまたはパターン集を見てみてください。

参考文献

https://guide.elm-lang.jp/appendix/types_as_sets.html
https://zenn.dev/uhyo/scraps/13760c3798d8ce

Discussion