Elmに学ぶ集合としての型
話すこと
概要
プログラミングにおいて、型を集合として考える視点は非常に重要です。この記事では、Elmの例を通して型を集合として捉える方法について解説します。
Elmにおける型と集合
まず、ElmにおけるCustomTypeを取り上げ、型を集合として理解する方法を探ります。CustomTypeは複数の異なるケースを持つ型を定義する機能です。これにより、異なるデータの集合を一つの型として扱うことができます。
CustomTypeの例
type Program
= Raymonda
| LaSylphide Bool
このコードではProgram
というCustomTypeが定義されています。この型にはRaymondaとLaSylphideの2つのバリアント※1があり、各バリアントは異なるデータを保持しています。
※1 バリアントとはカスタム型を構成する一つの値のことを指すと理解しています。
パターンマッチングによるCustomTypeの分解
Elmの強力な機能であるパターンマッチングを使えば、CustomTypeを分解して各バリアントにアクセスし関連するデータを取り出すことができます。
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の代替ではありません。
type alias LaSylphide =
{ isDenmark : Bool
}
集合としての型
型を集合として捉えると、各型に「濃度」が存在することがわかります。濃度とは、集合に含まれる要素の数、すなわち型に対応するデータの数を指します。
Programの例ではRaymondaとLaSylphideという2つのバリアントがあり、それぞれ異なるデータの組み合わせを持っています。Raymondaはデータを持たないため濃度は1、一方、LaSylphideはBool型のデータを持ち、2つの組み合わせが可能です。したがって、Program型の濃度は合計で3です。
Programバリアント | データ | データ数 |
---|---|---|
Raymonda | - | 1 |
LaSylphide | true / false | 2 |
濃度の乗算
型の濃度は乗算することができます。乗算はバリアントが保持するデータ間で行われます。以下の例を見てみましょう。
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。以下の例を考えてみましょう。
type Color
= Red
| Yellow
| Blue
Color型は、Red、Yellow、Blueの3つのバリアントで構成され、それぞれデータを持たないため、Colorの濃度は3です。
Colorバリアント | データ | データ数 |
---|---|---|
Red | - | 1 |
Yellow | - | 1 |
Blue | - | 1 |
※2 厳密には和の計算が行わえるのはCustomTypeの濃度だけではなく、TypeAliasやタプルでも和の計算が行われます。
濃度の意識の重要性
濃度を意識して型設計を行うことは、不要なパターンを排除し、コードをより明確にするために重要です。たとえば、ロード状態を表すデータ型を考えてみましょう。
type LoadingState a
= Loading
| Loaded a
このLoadingState型の濃度は一番少なくて2です。
LoadingStateバリアント | データ | データ数 |
---|---|---|
Loading | - | 1 |
Loaded | a | aのデータ数※3 |
次に、type aliasを使って近いものを表現してみます。
type alias LoadingState a =
{ isLoading : Bool
, data : Maybe a
}
この場合、濃度は最低でも4になります。
isLoading | data |
---|---|
false | Nothing |
false | Just a |
true | Nothing |
true | Just a |
このように、type aliasを使うと不要なデータの組み合わせが増え、処理の複雑さが増してしまいます。これは型の濃度を意識しなかった結果、濃度の乗算が行われているからです。
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を使って再実装します。
display : (a -> String) -> LoadingState a -> String
display toString loadingState =
case Loading -> "Loading"
case Loaded value -> String.join " " ["Loaded", toString value]
再実装後は分岐が2つになりました。また型の加算が扱えるようになったことで細かな型の濃度の調整も容易です。例えばロード状態に失敗の状態が増えた場合であってもCustomTypeにバリアントを一つ追加してあげれば終わりです。※4
type LoadingState a
= Loading
| Loaded a
| Failure
※3 Elmにおいて型定義に出てくる小文字は任意の型を表します。なのでこの表ではaのデータの数はわからないです。そのため数値ではなくaのデータ数と表記しています。
※4 だいぶと雑な例です。実際はLoadingStateに失敗のパターンを追加するのは意味的なところで違和感があるので、個人的には別の方法を取るのをお勧めします。
まとめ
この記事では、プログラミングにおける型を集合として捉える視点が、どのように型設計に影響を与えるかを探りました。特に、ElmのCustomTypeを例に、型が持つデータの組み合わせ(濃度)を理解することの重要性を解説しました。型の濃度を意識することで、無駄なパターンを排除し、シンプルで明確なコードを設計することが可能になります。
終わりに
TypeScriptもまた言語こそ異なりますがElmと同じフロントエンド領域における静的型付け言語です。
実際にElmで見かける型のテクニックはTypeScriptでも再現可能です。Elmから型のテクニックを学ぶのも良いのではないでしょうか。
- CustomType: TypeScriptでもDiscriminated Unionというテクニックとして利用できます。
- PhantomType: TypeScriptでもBranded Typeとして利用されています
- OpaqueType: TypeScriptでも再現可能(特に名称はついていない認識です)
Elmを通じて型を学んでみたいと思った方は是非ガイドラインまたはパターン集を見てみてください。
参考文献
Discussion