LoginSignup
13
3

More than 1 year has passed since last update.

Elmのカスタム型がわかんなくなったときに読む記事

Last updated at Posted at 2023-02-13

Elmを学び始めた方がよくつまづくポイントに「カスタム型」があります。
さくらちゃんも別の言語で最初にカスタム型のような概念に出会ったとき、頭がぐちゃぐちゃになりました。むじゅかしぃ😢

ということで、カスタム型につまづいた方の理解の助けになれるよう、解説していきます🖊

まずはEnumからはじめよう

別の言語でEnumとか列挙型とかいった概念を知っている方であれば、きっと以下の例までは理解しやすいと思います。

type Color
    = Red
    | Blue
    | Green

ここでは色を表すための独自のあたらしい型Colorを定義しています。
Red(赤)、Blue(青)、Green(緑)の3色を表現できます。

もちろん、文字列型を使って"Red" "Blue" "Green" と表現することもできますが、それでは誤って "Glue" みたいなデータを入れてしまう可能性があります。
厳密に「これらの値しかありえない」と分かっているなら、上記のようにとりうる具体的な値を決めた独自の型 カスタム型 を定義することでそういったミスを防げるわけです。

さて、この型定義は実はColorという を定義しているだけではありません。だって、型だけ定義してその型をもつ値をつくる方法がなかったら意味ないじゃないですか。
だから同時にRed Blue Green という も定義するのがこの構文の正体なんです。
言い方を変えれば、Red Blue Greenという Color型の 定数 を定義していることになります。

定数は小文字からはじまるはずなのに、これだけは大文字はじまりの定数です。ひどいことをしますね!💢🐐
イメージとしては、上記の型定義によって以下のような 定数 が定義されているということです。

Red : Color
Red = (なんちゃらかんちゃら)

Blue : Color
Blue = (なんちゃらかんちゃら)

Green : Color
Green = (なんちゃらかんちゃら)

そして、Elmにおいて 定数 とは「引数なしの関数」と同じだということを少し頭の隅に置いておいてください🤔

型引数をマスターしよう

ここからがつまづきポイントです。

さて、よくある会員登録が必要なWebサービスを考えてみましょう。
利用者の利便性を考えるなら、はじめは会員登録せずにつかえるようにしたほうが喜ばれます。
そうして「いいな」と思ってもらった段階で会員登録させてより多くの機能を使ってもらうのです。

そんなアプリケーションで会員を表す型をつくってみましょう。

type UserType
    = GuestUser
    | RegularUser

先ほどと同じようにとりうる値を列挙しただけです。

さて、ユーザーにはユーザーIDなどの情報も存在します。
愚直にそれを実現するなら以下のような型エイリアスを用意するのではないでしょうか。

type alias User =
    { userType : UserType
    , userId : UserId
    }

type UserType
    = GuestUser
    | RegularUser

type alias UserId = String

型エイリアスについてはElm guideの型エイリアスの項目をご覧ください。

でもここで問題があります。ログインしていないユーザーはユーザーIDを持たないんです。
もちろん、苦肉の策としてログインしていない場合はuserIdフィールドの値を空文字列にするような運用も可能です。
でも以下のような「間違った」状態が存在しうることには変わりません。

okasinaUser : User
okasinaUser =
    { userType = GuestUser
    , userId = "ゲストなのになぜかIDがあるよ"
    }

ユーザーIDが存在しないはずのGuestUser(ログインしていない場合)なのに、データ構造上はIDを持つことができてしまうのです。
Color型の例でカスタム型と文字列型を比較して「カスタム型にすると、ありえない値を作れないようにできる」みたいなことを言っておきながらこの体たらくです。カスタム型をつかったのに、結局ありえない値をつくれちゃってるじゃん!💢🐐

ちょっと道端の草でも食べて落ち着いてください🌻 そういう需要を満たすために、実はカスタム型を使って以下のように定義することができます。

type User
    = GuestUser
    | RegularUser UserId

type alias UserId = String

RegularUser(ログイン済みの場合)の方に、なんかUserId型(実態はString型)の値がくっついてますね?
こうやって書くと、User型を定義するとともに、以下のような 関数 が自動的に定義されます。

GuestUser : User
GuestUser = (なんちゃらかんちゃら)

RegularUser : UserId -> User
RegularUser userId = (なんちゃらかんちゃら)

GuestUser 定数 (無引数の 関数 でもありました)は前述のColor型で見たのと同じ形式です。

一方でRegularUserの方は引数を1つ持つ 関数 になっています。
「上記のような型定義をすると、こういう関数が自動的に定義されるよ」という単なるお約束だと思ってください。
そしてここで自動的に定義されたRegularUserという 関数 は、「引数で渡したUserId型の値を 内包した User型の値を返す」という関数です。

さて内包した とはどういうことでしょうか? 例えば以下のようにUser型の を定義してみます。

someUser : User
someUser = RegularUser "Sakura-chan"

このsomeUserという から、あとで"Sakura-chan"という値を取り出せるというのが、「内包した」の意味するところです。

内包した値を取り出してみよう

実際に取り出すには、case式を使います。User型の値を受け取って、そのユーザーのIDを画面表示用文字列に変換する関数を作ってみましょう。

displayUserId : User-> String
displayUserId user =
    case userId of
        GuestUser ->
            "ゲスト"

        RegularUser userId ->
            userId

こう定義すれば、以下のように使うことができます。

someUser1 : User
someUser1 = RegularUser "Sakura-chan"

someUser2 : GuestUser
someUser2 = GuestUser


displayUserId someUser1
--> "Sakura-chan"

displayUserId someUser2
--> "ゲスト"

これなら、「ログインしていないユーザーなのにIDを持っている」というありえない状態をつくれないように制限できますね🌷

もっと情報を内包させてみよう

さて、ログイン中のユーザーはユーザーIDの他にも氏名などを登録しているかもしれません。
そういう場合にも、以下のようにすれば対応できます。

type User
    = GuestUser
    | RegularUser UserId Name

type alias UserId = String
type alias Name = String

こうやって書くと、User型を定義するとともに、以下のような 関数 が自動的に定義されます。

GuestUser : User
GuestUser = (なんちゃらかんちゃら)

RegularUser : UserId -> Name -> User
RegularUser userId name = (なんちゃらかんちゃら)

今度は、User型の値を受け取って、そのユーザーの氏名を画面表示用文字列に変換する関数を作ってみます。

displayUserName : UserType -> String
displayUserName user =
    case userId of
        GuestUser ->
            "ゲスト様"

        RegularUser userId name ->
            name

こう定義すれば、以下のように使うことができます。

someUser1 : UserType
someUser1 = RegularUser "Sakura-chan" "ヤギのさくらちゃん"

someUser2 : GuestUser
someUser2 = GuestUser


displayUserId someUser1
--> "ヤギのさくらちゃん"

displayUserId someUser2
--> "ゲスト様"

もっともっと情報を内包させるには

同じように引数を増やしていけば、電話番号やツノの本数などの情報も内包させることができます。
でも、実用的には以下のような定義のしかたがオススメです。

type User
    = GuestUser
    | RegularUser Profile

type alias Profile =
    { userId : String
    , name : String
    , phone : String
    , horns : Int
    }

そう、レコードを内包させちゃえばいいのです。

まとめ

カスタム型のポイントは、裏で内緒で 関数 が自動的に定義されていることに気づくことです。

もしこの説明でわからないところがあれば、Twitterでさくらちゃんにメンションしてください。
Qiitaのコメント欄に質問などを書いていただいても見ないのでご注意ください。

では、よいElmライフを🌹

さくらちゃんのツイッターをフォローする
さくらちゃんが書いた他の記事を見る
さくらちゃんが翻訳したElmの本を手に入れる
さくらちゃんの写真集を手に入れる

goodbye.jpg

13
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
3