LoginSignup
26
8

More than 3 years have passed since last update.

Elmのmap関数について一通りまとめてみた

Last updated at Posted at 2019-07-28

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.

あるデータ構造の内部の値を変更して、そのデータ構造はそのままにするもの

Map in Elm | Medium

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は「内部のものを取り込んで変換を適用し、結果をデータ構造に再パッケージ化する」ためのもの

All over the map(s) | Medium

ざっくり以上のようなものが定義として挙げられている。

elmのmap関数一覧

抽象論に終始しないためにelmのmapにはどのようなものがあるのか具体を見てみる。公式に例がある場合は例も記載。ほとんどの型がmap関数を持っている印象。

Array
map : (a -> b) -> Array a -> Array b

map sqrt (fromList [1,4,9]) == fromList [1,2,3]
List
map : (a -> b) -> List a -> List b

map sqrt [1,4,9] == [1,2,3]
map not [True,False,True] == [False,True,False]
Dict
map : (k -> a -> b) -> Dict k a -> Dict k b
Set
map : (comparable -> comparable2) -> Set comparable -> Set comparable2
String
map : (Char -> Char) -> String -> String

map (\c -> if c == '/' then '.' else c) "a/b/c" == "a.b.c"
Tupple
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)
Decoder
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タイプとして切り出したデータ構造に対してデフォルト値を設定するときによく用いられる。

Maybe
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を返す
Result
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"
Task
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)

List
map : (a -> b) -> List a -> List b

map sqrt [1,4,9] == [1,2,3]
map not [True,False,True] == [False,True,False]
Generator
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として活用するときに用いる。

Cmd
map : (a -> msg) -> Cmd a -> Cmd msg
Sub
map : (a -> msg) -> Sub a -> Sub msg
Html
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.

必ずしも何かが型の「内部にある」必要はない、それは約束された何かにもなり得る

All over the map(s) | Medium

最初の項で、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のパイプ|>の良さ | ジンジャー研究室

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のいい点だと思う

以上。

26
8
0

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
26
8