elmの記事を読み漁っていたときに、mapはListやMaybeだけのものではなく全てのものに使うことができる、的な一文を目にしてとても気になったため、そもそもmapとは何か、elmのmapにはどのようなものがあるのか、mapの定義の再解釈、カスタムタイプを作るときのmapの活用法、そしてjsのmapとの利便性の違いみたいなところをまとめてみたい。
そもそもmap関数とは何かざっくり
List, Maybe, Result, Task, Decoder are all pieces of structure that hold onto values, we can use map on any of them to reach inside and change those values.
値を保持するデータ構造の内部にリーチしてそれらの値を変えることができるもの
map is not just for lists, you can use map to change the values inside structures like Maybe, Result and even Decoder whilst preserving that structure.
mapはlistのためのものだけではなく、データ構造を保持しつつ内部の値を変更することができるもの
a way to apply a function over or around some structure that we don’t want to alter.
That is, we want to apply the function to the value that is “inside” some structure and leave the structure alone.
あるデータ構造の内部の値を変更して、そのデータ構造はそのままにするもの
Ok… so map is a kind of generic name for “take what’s inside and apply a transformation, and repackage the results into the structure
mapは「内部のものを取り込んで変換を適用し、結果をデータ構造に再パッケージ化する」ためのもの
ざっくり以上のようなものが定義として挙げられている。
elmのmap関数一覧
抽象論に終始しないためにelmのmapにはどのようなものがあるのか具体を見てみる。公式に例がある場合は例も記載。ほとんどの型がmap関数を持っている印象。
map : (a -> b) -> Array a -> Array b
map sqrt (fromList [1,4,9]) == fromList [1,2,3]
map : (a -> b) -> List a -> List b
map sqrt [1,4,9] == [1,2,3]
map not [True,False,True] == [False,True,False]
map : (k -> a -> b) -> Dict k a -> Dict k b
map : (comparable -> comparable2) -> Set comparable -> Set comparable2
map : (Char -> Char) -> String -> String
map (\c -> if c == '/' then '.' else c) "a/b/c" == "a.b.c"
mapFirst : (a -> x) -> ( a, b ) -> ( x, b )
mapFirst String.reverse ("stressed", 16) == ("desserts", 16)
mapFirst String.length ("stressed", 16) == (8, 16)
mapSecond : (b -> y) -> ( a, b ) -> ( a, y )
mapSecond sqrt ("stressed", 16) == ("stressed", 4)
mapSecond negate ("stressed", 16) == ("stressed", -16)
mapBoth : (a -> x) -> (b -> y) -> ( a, b ) -> ( x, y )
mapBoth String.reverse sqrt ("stressed", 16) == ("desserts", 4)
mapBoth String.length negate ("stressed", 16) == (8, -16)
map : (a -> value) -> Decoder a -> Decoder value
stringLength : Decoder Int
stringLength =
map String.length string
Maybe.mapの考え方はResult.mapとかでも共通する、とても便利。
基本的な捉え方は、「第二引数がNothing相当のものだった場合、第一引数の関数を適用せず、そのNothing相当のものをそのまま返す」感じ。Maybe.mapでNothingが返ることを想定すれば、Maybe.withDefault
を|>
で繋いであげると、case文
でパターンマッチしてJust xx
の中身を取り出してNothing
の場合の値をベタベタに書くよりもっさり感が減る。
このデフォルト値の設定方法に関しては、Opaqueタイプとして切り出したデータ構造に対してデフォルト値を設定するときによく用いられる。
map : (a -> b) -> Maybe a -> Maybe b
map sqrt (Just 9) == Just 3
map sqrt Nothing == Nothing
map sqrt (String.toFloat "9") == Just 3
map sqrt (String.toFloat "x") == Nothing
mapRes = String.toFloat "x" -- -> Nothing
|> Maybe.map squrt -- -> Nothing
|> Maybe.defaultWith 0 -- Nothingなのでデフォルトの0を返す
map : (a -> value) -> Result x a -> Result x value
map sqrt (Ok 4.0) == Ok 2.0
map sqrt (Err "bad input") == Err "bad input"
map : (a -> b) -> Task x a -> Task x b
timeInOneHour : Task x Time.Posix
timeInOneHour =
Task.map addAnHour Time.now
addAnHour : Time.Posix -> Time.Posix
addAnHour time =
Time.millisToPosix (Time.posixToMillis time + 60 * 60 * 1000)
map : (a -> b) -> List a -> List b
map sqrt [1,4,9] == [1,2,3]
map not [True,False,True] == [False,True,False]
map : (a -> b) -> Generator a -> Generator b
bool : Generator Bool
bool =
map ((==) 1) (int 0 1)
lowercaseLetter : Generator Char
lowercaseLetter =
map (\n -> Char.fromCode (n + 97)) (int 0 25)
uppercaseLetter : Generator Char
uppercaseLetter =
map (\n -> Char.fromCode (n + 65)) (int 0 25)
Cmd, Sub, HtmlのmapはSPAを作るとすると多用するイメージ。
具体的には子モジュールのMsgを親モジュールのMsgとして活用するときに用いる。
map : (a -> msg) -> Cmd a -> Cmd msg
map : (a -> msg) -> Sub a -> Sub msg
map : (a -> msg) -> Html a -> Html msg
type Msg = Left | Right
view : model -> Html Msg
view model =
div []
[ map (\_ -> Left) (viewButton "Left")
, map (\_ -> Right) (viewButton "Right")
]
viewButton : String -> Html ()
viewButton name =
button [ onClick () ] [ text name ]
ここでmapの定義を敷衍してみる
mapの記事を読み漁る中で興味深い一文を見つけた。
what I wanted you to see is that map does not necessarily need for something to be “inside” the type it works with. It can be something that’s “promised” too.
必ずしも何かが型の「内部にある」必要はない、それは約束された何かにもなり得る
最初の項で、mapとはデータ構造の内部の値を変換して新しいデータ構造を作るもの、というざっくりした概念を紹介したが、jsでいうPromiseのように何かが返ってくると約束されたもの、つまり、elmにおいて副作用として扱われるI/O処理やAPIとの通信などの非同期処理と相性がいいみたいだということがわかった
Result、Generator、Cmd、Subなどがそれに当たる
つまり、mapとはデータ構造の内部の値だけではなく約束された副作用の結果の値を扱うものだと再解釈できる。
mapのカスタムタイプにおける活用
mapはカスタムタイプを定義して扱うときにもうまく使えそうだと感じる
以下のバグの少ないUIのためのリモートデータの扱いに関する記事が面白い
Elm's Remote Data Type in Javascript | Dev.to
まずカスタムタイプを定義する
type RemoteData e a
= NotAsked
| Loading
| Error e
| Success a
Modelを定義し、初期値をNotAsked(上記で述べたNothing相当のもの)にしておく
type alias Model = RemoteData String MyData
-- and set the initial model value as NotAsked
init = NotAsked
view model =
case model of
NotAsked -> div [] [ text "Not asked yet" ]
Loading -> div [] [ text "Loading..." ]
Error err -> div [] [ text err ]
Success data -> div [] [ text <| "Here is my data: " ++ data ]
こうすることでバグの少ないUI設計ができることに加えてmapとwithDefaultを定義してみると
map : (a -> b) -> RemoteData e a -> RemoteData e b
map transform remoteData =
case remoteData of
Success data ->
Success (transform data)
_ ->
remoteData -- この辺りのコードは用途によって書き分ける
withDefault : a -> RemoteData e a -> a
withDefault default remoteData =
case remoteData of
Succeess data -> data
_ -> default -- この辺りのコードは用途によって書き分ける
ラップしたままデータ構造を扱える便利な関数ができると思った
jsのmapとの利便性の違い
最後にジンジャーさんの記事を紹介したい
elmにおいてデータの流れをパイプ|>
とmapで繋いで行くときのjsとelmの違いについて書かれている。以下少し抜粋させていただく
const promises =
[1,2,3].map(a => a + 1).map(toPromise);
Promise.all(promises); // promisesは配列なのでpromises.allと書けない
jsでは既存の型にはない関数を追加するにはprototype拡張が必要になるが
[1,2,3]
|> List.map (\a -> a + 1)
|> Debug.log "converted" -- List に対して Debug モジュールの関数を使う
|> List.map toString
|> MyListUtil.getByIndex 1 -- List に対して MyListUtil モジュールの関数を使う
|> Debug.log "result" -- Maybe に対して Debug モジュールの関数を使う
elmだと気にせずどんどん連鎖できるのはjsと比べたときのelmのいい点だと思う
以上。