わざわざ学んでみたらElmのMaybe型が楽しかった


わざわざ学ばなくてもいい言語ランキング1位の「Elm」をわざわざ学んでみるの続編ですわ。

前回のあらすじ

コンパイルしてJavaScriptに変換することができるAltJS的な純粋関数型言語Elm、その基礎文法を覚えたワイ。
Hello Worldの次は、簡単な双方向バインディングに挑戦することにしたが・・・?

型が楽しくなってきたワイ

ワイ「ちゃんと型を定義すると、ミスが発生しにくくなるからええな」
ワイ「例えばtakashiage36を代入しようとした時に」
ワイ「間違ってhageとかタイポしてしまったとしても」

型の不一致
ヒント:レコードフィールドの間違いのようです。
たぶんhageageであるべきでは?

ワイ「とかいって、Elmコンパイラが分かりやすく指摘してくれるからな」
ワイ「間違ったことしてたらそもそも先に進めへん」
ワイ「せやから、実際にプログラムを動かしてみる前にエラーを治せてしまうんやな」
ワイ「Elmは実行時エラーが出ないってのが売りやもんな」

型について疑問

ワイ「型を定義したら、その通りの値しか代入できひんってのは分かったけど」
ワイ「ユーザーが変な値を入力してきてもうたらどうなんねやろ」
ワイ「例えばワイは今回」

ユーザーがinputタグに数値を入力したら、
その数値が他のDOMにも表示される

ワイ「↑こういう、いわゆる双方向バインディングをしてみたいんや」
ワイ「せやから、数値だけを入力して欲しいんや」
ワイ「でも、もしユーザが」

文字列を入力してみるンゴwww

ワイ「とかいう文字列を入力してきたら、どうなんねやろ」
ワイ「ワイがいくらuserInputNumberInt型や!とか定義してても」
ワイ「実際ユーザが何を入力してくるかは分からへんもんな」
ワイ「やっぱ文字列を入力されたら、流石にエラー出てしまうんか・・・?」
ワイ「いや、でもElmでは実行時エラーは出ないらしいしな・・・」

ワイ「静的型付言語は、実行前にコードを静的に検査して」
ワイ「不正なコードがあればコンパイルエラーで教えてくれるけど」
ワイ「ユーザがどんな値を入力してくるか分からんのに」
ワイ「どうやってそれを事前に検査できるんやろ・・・」
ワイ「気になるから、とりあえずやってみよか」

viewinputタグを設置

ワイ「えーと、inputタグに何かが入力されたら」
ワイ「それを検知して何らかの処理を走らせればええから・・・」

view model =
    div []
        [ input [ onInput UserInput ] [] ]

ワイ「viewの中身は↑こうやな!」

input [ onInput UserInput ] []

ワイ「↑この一番左のinputは、inputタグを生成してくれや、ってことや」
ワイ「ほんで[ onInput UserInput ]いうのは」
ワイ「ユーザが何か入力したらUserInputというメッセージを送ってくれや、ってな意味や」
ワイ「そのメッセージはどこに送られるかというと」
ワイ「updateという関数に送られるんや」
ワイ「せやから次はupdateいう関数を定義せなあかん」
ワイ「UserInputというメッセージが来たら」
ワイ「どんな風に状態を更新するか、それを定義していくで」

update msg model =
    case msg of
        UserInput inputString ->
            { model | userInputNumber = String.toInt inputString }

ワイ「↑こうやな」
ワイ「UserInputいうメッセージが来た場合は」
ワイ「modeluserInputNumberに」
ワイ「ユーザが入力した数値を入れてやんねん」

ワイ「ただ、ユーザが入力した数値は"3"とか"8"とかいう文字列のはずやから」
ワイ「String.toIntいう関数でStirng型からInt型に変換せなあかんねん」
ワイ「userInputNumberにはInt型の値しか入れられへんからな」
ワイ「ちゃんと変換してあげればコンパイラちゃんも安心や」

型の不一致
userInputNumberフィールドをこのように更新することはできません。
toInt呼び出しは以下を生成します。

Maybe Int

ワイ「ファッ!?
ワイ「コンパイルエラー出てもうた」
ワイ「String.toIntは文字列をInt型に変換してくれるんちゃうんか」
ワイ「Maybe Int型にしてしまうんか」
ワイ「何や、たぶん整数て」

ピー君「だって数値に変換できひんかもんしれんやん」

ワイ「おぉ、インコピー君やないか」
ワイ「数値に変換できひんかもんしれへんてどういうこと?」

ピー君「お前も言うてたけど、ユーザが」

文字列を入力してみるンゴwww

ピー君「とかいう文字列を入力してくるかもしれへんやん」
ピー君「数値に変換できる文字列ばかりとは限らんわけや」
ピー君「せやからString.toIntいう関数はMaybe Int型の値を生成するんや」

ワイ「分かったわ」
ワイ「ほなuserInputNumberの定義を変えるわ」

type alias Model =
    { userInputNumber : Maybe Int
    }

ワイ「こうやな」
ワイ「userInputNumberMaybe Intの値が入るべきですよ、と」

ワイ「ありゃ、またエラーが出たで」

型の不一致
init定義の本体に何か問題があります。

ワイ「状態の初期値を定義するinitに問題があんのかいな」

init : Model
init =
    { userInputNumber = 0
    }

ワイ「ああ、そうか」
ワイ「さっきuserInputNumberMaybe Int型にすることにしたから」
ワイ「Int型の0という数値を入れたらアカンのか」

ワイ「Maybe Int型の初期値って、何を入れたらええんや・・・」

ピー君「Maybe Int型の値は」
ピー君「Just 0とかJust 5みたいな値か」
ピー君「Nothingのどちらかや」

ワイ「ほえ〜」
ワイ「0やなくてJust 0なんやな」
ワイ「ほなinitの中身は」

init =
    { userInputNumber = Just 0
    }

ワイ「↑こうやな」
ワイ「ほんまや、エラー消えたわ」
ワイ「これで、ユーザが何か入力するたびに、その値がModelに格納されるわけやな」
ワイ「ほなピー君、その新しい値を画面に表示するにはどうすんの?」

ピー君「view関数の中にmodel.userInputNumberって書くだけや」

ピー君「ユーザの入力をきっかけに」
ピー君「UserInputいうメッセージがupdate関数に届けられると、」
ピー君「update関数が新しい状態を作るやろ?」
ピー君「そうすると自動的に再度view関数が実行されるんや」
ピー君「せやからview関数の中に」
ピー君「model.userInputNumberを表示するためのタグを書いとけばええんや」

ワイ「なるほどな」
ワイ「でも、一つ気に何なんねんけど・・・」
ワイ「そのMaybe Intとか言う値はさあ」
ワイ「数値やないかもしれへんねやろ?」
ワイ「だって、ユーザが」

文字列を入力してみるンゴwww

ワイ「とかいう文字列を入力してくるかもしれんわけやから」
ワイ「その場合はmodel.userInputNumberには何が入ってくるん」

ピー君「その場合はNothingいう値が入ってくるわ」
ピー君「String.toInt関数で、Just 3とかJust 8とかに変換できひんかった場合は」
ピー君「Nothingいう値になんねん」

ワイ「そうなんか・・・」
ワイ「えぇ・・・でも・・・」
ワイ「数値が入っとるかもしれへんし、何も入ってへんかもしれへん・・・
ワイ「そんなシュレーディンガーの猫みたいな値を画面に表示しようとしたらなんかバグりそうやん・・・」
ワイ「undefinedとかになりそうやん・・・」

ピー君「とりあえずやってみい」

ワイ「とりあえずやるんかい」
ワイ「じゃあ、画面に表示するためにString.fromInt言う関数を使って」
ワイ「model.userInputNumber文字列型に変換して表示させるわ!」

p [] [ text (String.fromInt model.userInputNumber) ]

ワイ「こうや」
ワイ「pタグを生成して、その中で」
ワイ「文字列に変換したmodel.userInputNumberを表示や!」
ワイ「どや、コンパイラはん!?」

型の不一致
fromIntの最初の引数にはIntが必要です。
userInputNumberの値は次のとおりです。

Maybe Int

ワイ「せやろな」
ワイ「何となく分かってたわ」
ワイ「fromIntって名前の関数やもん」
ワイ「Maybe Int渡したらアカンわ」
ワイ「ほなどうしたらええんや・・・」

ワイ「このシュレーディンガーの猫みたいな」
ワイ「数値なのか何もないんか、両方の可能性を持つ値を」
ワイ「どうやって扱えばええんや・・・」

ピー君「caseを使って、両方の可能性に対応した処理を書くんや」

ワイ「両方の可能性に対応した処理・・・」
ワイ「よう分からんけどcaseを使ってやってみるわ」

case maybeInt of
    Just int ->
        String.fromInt int

    Nothing ->
        "数値以外はやめてや"

ワイ「こんな感じ?」
ワイ「数値やったらString.fromIntで文字列に変換するし」
ワイ「Nothingやったら"数値以外はやめてや"って文字列を返す」

ピー君「ええやん」
ピー君「そんな感じでcaseを使って」
ピー君「シュレーディンガーの猫ちゃんを密閉状態の箱の中から出してあげるんや
ピー君「それを関数にしてみい」

ワイ「分かったわ」

maybeIntToString maybeInt =
    case maybeInt of
        Just int ->
            String.fromInt int

        Nothing ->
            "数値以外はやめてや"

ワイ「Maybe Intを引数に受け取って、文字列を返す関数やから」
ワイ「関数名はmaybeIntToStringや」
ワイ「型注釈でいうとmaybeIntToString : Maybe Int -> Stringやな」

ピー君「その関数を使ってmodel.userInputNumberを表示するんや」

ワイ「おー、だんだん分かってきたで」

p [] [ text (maybeIntToString model.userInputNumber) ]

ワイ「こういうことやな」

動かしてみる

ワイ「おお!」
ワイ「ようやくコンパイルが通ったで!」
ワイ「inputタグに数値を入力すると、pタグにも同じ数値が表示されるし」
ワイ「inputタグに文字列を入力してみるンゴwwwって入力した場合は」
ワイ「pタグに数値以外はやめてやって表示されるわ」

ワイ「ちなみにcaseで片方のケースしか書かんかったらどうなんねやろ」
ワイ「Nothingの方を消してみよ」

maybeIntToString : Maybe Int -> String
maybeIntToString maybeInt =
    case maybeInt of
        Just int ->
            String.fromInt int

ワイ「おお、ちゃんとコンパイルエラーになるんやな」

欠けているパターン
このcaseはすべての可能性のための分岐を持ちません:
行方不明の可能性が含まれます:

Nothing

ワイ「なるほどな〜」
ワイ「シュレーディンガーの猫ちゃん的な値を扱うときは」
ワイ「caseを使って、考えられ得る全てのパターンを網羅した処理を書かんと」
ワイ「そもそもコンパイルが通らへんのか1
ワイ「こうすることで、ユーザ入力という不確定性のある値でさえも」
ワイ「事前に検査できるわけか」
ワイ「むっちゃオモロイやーん

感想

ワイ「型注釈を書くと、JSDocsに相当するような情報がコード上に残っていくことになるから」
ワイ「後からコードを読んだときに意味が分かりやすいな」
ワイ「しかもその注釈の通りにコンパイラが監視して、間違ってたら分かりやすく教えてくれる・・・」
ワイ「ただの注釈やなくて、強制力のある注釈ってわけや」

ワイ「あとcaseとかMaybe型がオモロイな」
ワイ「Maybe型という、そのままでは表示できない変な型を持った値」
ワイ「何が入ってるか普通には見えへん、密閉された箱に入ったような値

ワイ「そういう値はcase全ケースの処理を網羅せんと、中の値を取り出すことができひん1
ワイ「それによって、シュレディンガーの猫ちゃん的な不確定な値も」
ワイ「事前にバッチリ検査できんねんな」
ワイ「そら実行時エラーも起きませんわ
ワイ「さすが、言語ランキング1位Elmちゃんや」
ワイ「どういうランキングやったっけな・・・」
ワイ「ちょっと調べてみよ」

2019年にわざわざ学ばなくてもいいプログラミング言語
総合1位: Elm

ワイ「学ばなくていいんかい!
ワイ「めっちゃ学んでもうたわ!!!

ワイ「・・・ていうか」
ワイ「こんな素晴らしい言語がワースト1位って・・・」

ワイ「世界どうなってんねん!!!」

〜おしまい〜

追記1: 今回のコード

Ellieで見れるから好きにいじってみてや。

追記2: ビギナーにお勧めのElm記事たち

Elmで体験する関数型言語の面白さ
VS Code で Elm の開発環境を構築する on Windows 10
値が制約を満たしていることを型で保証する

追記3: 後日談

ワイ「ところでインコピー君は」
ワイ「いつから会話ができるようになったんやろ」
ワイ「インコってオウム返し、もといインコ返ししかできひんはずやん」

ハスケル子「私が吹き込んでおきました」

ワイ「おお」
ワイ「天才中学生インターンのハスケル子ちゃん・・・」

ハスケル子「やめ太郎さんがElmの勉強してるなあ、と思って」
ハスケル子「あらかじめ想定される疑問について録音しておきました

ワイ「そうなんか・・・相変わらず神がかってるな・・・」
ワイ「でもハスケル子ちゃん・・・」
ワイ「録音て
ワイ「ピー君をテープレコーダーみたいにいうたらアカンで」

ハスケル子「テープレコーダーって何ですか」

ワイ「・・・んんジェネレーションギャップ!!!

〜おしまい〜


  1. ほかにもwithDefaultとかでも値を取り出せます