Open15

The Elm Archtecture

ShebangDogShebangDog

typetype aliasの違いは?

type

typeがなんなのかはわかっていない。
使われ方としてはsealed classに似ている気がする

type.elm
type Either
 = Loading
 | Success { value: Int }
 | Failure { message: String }

getValue : Either -> String
getValue either = 
  case either of
    Loading -> "loading"
    Success v -> "success"
    Failure message -> "failure"

type alias

type aliasは型に名前をつけるもの

typeAlias.elm
type alias T = { value: Int }
ShebangDogShebangDog

typeはカスタム型の宣言に利用するみたいだ
また別名がtagged unionやADTと呼ばれていることからseald classはあながち間違っていなそう

ShebangDogShebangDog

type aliasはレコードを作ってる

typeAlias.elm
type alias T = { value : Int } 

T // <function> : Int -> T 
typeAliasLike.elm
record : Int -> T
record first = { value = first }

record // <fucntion> : Int -> { value : Int }

typeAlias.elmtypeAliasLike.elmはほぼほぼ同じ

ShebangDogShebangDog

type aliasはレコードを作る際に、コンストラクタを生成する

ShebangDogShebangDog

typeもtype aliasもコンストラクタは作ってそう

コード
constructor
type T
    = A
    | B String
    | C String Int

a : T
a = A

b : String -> T
b = B

c : String -> Int -> T
c = C

type alias TA =
    { 
    }

type alias TB =
    { a : String
    }

type alias TC =
    { a : String
    , b : Int
    }

ta : TA
ta = TA

tb : String -> TB
tb = TB

tc : String -> Int -> TC
tc = TC
ShebangDogShebangDog

scalaでいうunapply?等を作ってくれるか作ってくれないかの違いはありそう

なんか違うかも?recordであれば以下で分解できる。
let {x, y} = position

variantならcase ofで分解?できる

ShebangDogShebangDog

エラーハンドリング

型にエラー情報を逃す。ResultMaybeでそれを実現する

Maybe

JavaのOptionと同等のものでnull参照をなくす

maybe.elm
type MaybeInt
  = Value number
   | Nothing

toInt : String -> MaybeInt
toInt string = 
  case String.toInt string of
    Nothing -> Nothing
    Just value -> Value value

Result

よく聞くEither
例外処理用の型

result.elm
type Either value error
 = Just value
  | Err error
ShebangDogShebangDog

#コマンドとサブスクリプション

Html msgでHTMLエレメントからメッセージを送るように、Cmd msgでコマンドからメッセージを送れる。

ShebangDogShebangDog

そもそもElmが最大限生かされる書き方ってどんなんなんだろう?
MVCを深く知ると良いのかな??

ShebangDogShebangDog

Lens触ってみた。真価はわからない

import Html exposing (text)
import Monocle.Lens exposing (Lens, modify)


type alias User =
      { name : String
      , age : Int
      }

user : User
user = User "" 1

userNameLens : Lens User String
userNameLens = 
  let 
    get u = u.name
    set n u = { u | name = n }
  in
  Lens get set
  
userAgeLens : Lens User Int
userAgeLens = 
  let 
    get u = u.age
    set a u = { u | age = a }
  in
  Lens get set

increment : Int -> Int
increment value = value + 1

main = text (String.fromInt ((modify userAgeLens increment) user).age)
ShebangDogShebangDog

対象読者

  • TypeScriptで型に触れ、型に興味を持った方
  • 型の学習方法に迷っているTypeScriptユーザー

話すこと

  • 型の濃度
  • 型学習におけるElmや他の言語のすすめ

話さないこと

  • ElmとTypeScriptの型の違い

結論

型のテクニックはTypeScriptだけではなく、他の言語から学べることがおおいので他の言語から学ぶのもおすすめします。

概要

プログラミングにおいて、型を集合として考える視点は非常に重要です。この記事では、Elmの例を通して、型を集合として捉える方法について解説します。さらに、TypeScriptでも同様のアプローチがあることを示します。

Elmにおける型と集合

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

CustomTypeの例

type Program
    = Raymonda
    | LaSylphide Bool

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

※1 バリアントとはカスタム型を構成する一つの値のことを指すと理解しています。TypeScriptに同等の使われ方がされるものは言語レベルではない気がします。同じ機能を有したものの再現はできます。

パターンマッチングによる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]

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

TypeScriptとCustomType

Elmに存在するCustomTypeは、TypeScriptでもDiscriminated Unionというテクニックとして利用できます。Elmや他の言語で学んだ型のテクニックは、TypeScriptで同様に再現可能です。※4 さらに、Elmや他の言語で使用されている多くの型のテクニックをTypeScriptに輸入して活用することができます。

たとえば、Branded Typeは、Elmや他の言語が以前から持っていた技術であり、TypeScriptでも活用されています。

したがって、型に関する知識を深めるためには、TypeScript以外の言語からも学ぶことが有益です。その第一歩として、Elmを使ってみるのはいかがでしょうか。

※4 全部ではない。ただし大抵の事はできそう。現に型レベルでの四則演算HKTなどの抽象度の高い型などの再現はできている。

まとめ

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

また、TypeScriptでもElmのような型のアプローチを再現できることを示し、他の言語で培った型の技術をTypeScriptに輸入できることについても触れました。Elmを通じて型を学んでみたいと思った方は是非ガイドラインを見てElmを始めてみてください。

参考文献

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