Maybeの基本的な使い方
「Maybe」という名前からもわかるように、Maybeを使うと「ないかもしれない」状態を表現することができる。
Maybeの定義は、Just
(ある場合)とNothing
(ない場合)の2状態からなるCustomTypeである。
type Maybe a
= Just a
| Nothing
CustomTypeでcase文を用いて引数を取り出せるのと同様に、Maybeもcase文で値を取り出すことができ、ある場合とない場合で処理を分けることができる。
例)名前がある時はその名前を、名前がない時は"NO NAME"と出力する
getUserName : Maybe String -> String
getUserName userName =
case userName of
Just name ->
name
Nothing ->
"NO NAME"
result =
Debug.log "result"
( getUserName (Just "yuna")
, getUserName Nothing
)
-- result: ("yuna","NO NAME")
Maybe.withdefault
Nothing
だった時の初期値を与えることができる。
先ほどの例のgetUserName
はMaybe.withdefault
を使うと以下のように書ける。
getUserName : Maybe String -> String
getUserName userName =
-- Justの場合: Justの引数の文字列が返る
-- Nothingの場合: "NO NAME"という文字列が返る
userName
|> Maybe.withDefault "NO NAME"
Maybe.map
Just
だった時の値をmapして変換することができる。Nothing
だった時は何もせずNothing
をそのまま返す。
hello : Maybe String -> Maybe String
hello userName =
userName
|> Maybe.map (\name -> name ++ "さんこんにちは!")
result =
Debug.log "result"
( hello (Just "yuna")
, hello Nothing
)
-- result: (Just "yunaさんこんにちは!",Nothing)
Maybe.map2, map3,...
複数要素が全てJust
の場合のみ値をmapして変換することができる。複数要素のうち1つでもNothing
であればNothing
を返す。
introduce : Maybe String -> Maybe Int -> Maybe String
introduce name age =
Maybe.map2 introduceStr name age
introduceStr : String -> Int -> String
introduceStr name age =
"名前は" ++ name ++ "です。" ++ (String.fromInt age) ++ "歳です。"
result =
Debug.log "result"
( introduce (Just "yuna") (Just 22)
, introduce Nothing (Just 22)
, introduce (Just "yuna") Nothing
)
-- result: (Just "名前はyunaです。22歳です。",Nothing,Nothing)
Maybe.withdefaultとMaybe.mapを組み合わせる
冗長になりがちなcase文を使わずにMaybeを扱うことができる。
case文を用いた書き方
hello : Maybe String -> String
hello userName =
case userName of
Just name ->
name ++ "さんこんにちは!"
Nothing ->
"無効なユーザーです"
Maybe.withdefaultとMaybe.mapを組み合わせた書き方
hello : Maybe String -> String
hello userName =
userName
|> Maybe.map (\name -> name ++ "さんこんにちは!")
|> Maybe.withDefault "無効なユーザーです"
上記の例は説明のための例であまり実用性がないので、アプリケーション開発で実際に使えそうな例を2つ示しておく。
例1)名前がある場合のみ画面に名前を表示する
viewUserName : Maybe String -> Html msg
viewUserName userName =
userName
|> Maybe.map (\name -> div [] [text <| "user: " ++ name])
|> Maybe.withDefault (text "")
例2)名前がある場合のみ年齢を取得するためのコマンドを発行する
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GetAge ->
let
cmd =
model.userName
|> Maybe.map (\name -> getAge name)
|> Maybe.withDefault (Cmd.none)
in
( model, cmd )
port getAge : String -> Cmd msg
変数の命名
Maybeの変数には変数名の頭に「maybe」とつけると、Maybe型であることがわかりやすいのでオススメ。
hello : Maybe String -> String
hello maybeUserName =
maybeUserName -- ここはMaybe型
|> Maybe.map (\name -> name ++ "さんこんにちは!") -- ここでのnameはJustの値
|> Maybe.withDefault "無効なユーザーです"
Maybeの多用に要注意
実際のアプリケーション開発では、例えばユーザがログインしている/いない、データを取得できている/いないなど、様々な場面でMaybeが活躍する。
Maybeは非常に便利だが、便利ゆえに大きいアプリケーションを作っていると、Maybeがどんどん増えてMaybe地獄になる。modelが持っているデータがほとんどMaybe型なんてことも...。
安易にMaybe型を使わず、よりわかりやすい表現がないか考えてみよう。
パターン1)ユーザがログインしている/いない
-- Maybeを使った場合
currentUser : Maybe User
-- CustomTypeを使ってログインしている/いないを表現
type CurrentUser
= LoggedIn User
| NotLogin
currentUser : CurrentUser
パターン2)データを取得できている/いない
-- Maybeを使った場合
users : Maybe (List User)
-- CustomTypeを使ってユーザ一覧情報の取得ができている/いないを表現
type Users
= Loaded (List User)
| NotLoaded
users : Users
上記の例を見て、「別に2パターンしかないならMaybeでよくね?いちいち型作るのメンドクセーじゃん!」と思うひともいるかもしれない。
しかし、Maybeは「ある/ない」しか表現できない。その「ある/ない」が、ログインできていないからないのか、ロード中だからないのか、といった「ある/ない」のコンテキストを失った型表現になっている。安易にMaybeを使わないメリットは、最初に実装した人がどういうコンテキストでその型を作ったかが明確になるというところにある。
例えば
users : Maybe (List User)
だと、usersがある時ってどういう状態のとき?ない時ってどういう状態のとき?ということが定義だけからは読み取ることができず、仕様やコード全体からそのコンテキストをくみ取らなければいけなくなる。これが
type Users
= Loaded (List User)
| NotLoaded
と書かれていると、「あ〜ロード中だからデータがないのか、なるほど」と他の実装者にもすぐに理解されるだろう。
もちろんコンテキストが自明な場合もあるので、私が言いたいのは「Maybeを使うな」ということではなく、「思考停止で安易にMaybeを使うのはやめよう!」ということです!