Elmで冗長な書き方をしていたデコーダなどをリファクタリングしたメモ

## 概要

[Google Spread Sheetで複数列を取得してElmのモデルに入れたメモ](https://qiita.com/hibohiboo/items/d8a910a7cb1382e92b15)
で問題としていた、以下のことについてツイッターでアドバイスをいただいたので反映してみる。

* ～でなければのときの書き方が冗長になっている。

[この時点のソース](https://github.com/hibohiboo/garden/tree/af5722cc04bfa29679088a8c1ddae6be6c252f1d)  

## アドバイス

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://t.co/XisWqUI7wI">https://t.co/XisWqUI7wI</a><br><br>これくらいですかね</p>&mdash; 猫背ステッチ（みやも） (@miyamo_madoka) <a href="https://twitter.com/miyamo_madoka/status/1122323940523663360?ref_src=twsrc%5Etfw">2019年4月28日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">CardData型のほうですが、面倒かつModelが増えますが、CardId.elm をつくってtype CardId = CardId StringしてfromStringやdecoderを定義しておくなどするのがいいのではないかと思います</p>&mdash; 猫背ステッチ（みやも） (@miyamo_madoka) <a href="https://twitter.com/miyamo_madoka/status/1122324534395162625?ref_src=twsrc%5Etfw">2019年4月28日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## リファクタリングしたソース

### Card.elm

* Tagを別のファイルに
* CardIdを別ファイルにして定義

```diff:Models/Card.elm
@@ -1,7 +1,6 @@
 port module Models.Card exposing
     ( CardData
     , CardLabelData
-    , Tag
     , cardDataListDecodeFromJson
     , cardDecodeFromString
     , cardDecoder
@@ -9,10 +8,6 @@ port module Models.Card exposing
     , illustedBy
     , initCard
     , skillCard
-    , tag
-    , tagDecoder
-    , tagParser
-    , tagsDecoder
     )
 
 import Browser.Dom as Dom
@@ -26,6 +21,8 @@ import Http
 import Json.Decode as D exposing (..)
 import Json.Decode.Pipeline exposing (custom, hardcoded, optional, required)
 import Json.Encode as E
+import Models.CardId as CardId exposing (CardId, decoder, fromString, toString)
+import Models.Tag exposing (Tag, tagsDecoder)
 import Session
 import Skeleton exposing (viewLink, viewMain)
 import Task exposing (..)
@@ -49,7 +46,7 @@ type alias CardLabelData =
 
 
 type alias CardData =
-    { cardId : String
+    { cardId : Maybe CardId
     , cardName : String
     , cardType : String
     , kind : String
@@ -73,14 +70,8 @@ type alias CardData =
     }
 
 
-type alias Tag =
-    { name : String
-    , level : Int
-    }
-
-
 initCard =
-    CardData "" "" "" "" 0 "" 0 0 0 "" 0 "" "" [] "" "" "" "" "" "" 0
+    CardData (CardId.fromString "") "" "" "" 0 "" 0 0 0 "" 0 "" "" [] "" "" "" "" "" "" 0
 
 
 
@@ -92,7 +83,7 @@ initCard =
 skillCard =
     let
         cardData =
-            CardData "B-001" "走る" "能力" "基本能力" 10 "アクション" 4 0 0 "自身" 1 "移動1" "逃げてもいいし、向かってもいい。\n君たちは何処にだっていける。\n一歩ずつではあるけれど。" [ Tag "移動" 0, Tag "基本能力" 0 ] "/assets/images/card/main/run.png" "ヒューマンピクトグラム2.0" "http://pictogram2.com/" "/assets/images/card/frame/report.gif" "" "https://google.com" 0
+            CardData (CardId.fromString "B-001") "走る" "能力" "基本能力" 10 "アクション" 4 0 0 "自身" 1 "移動1" "逃げてもいいし、向かってもいい。\n君たちは何処にだっていける。\n一歩ずつではあるけれど。" [ Tag "移動" 0, Tag "基本能力" 0 ] "/assets/images/card/main/run.png" "ヒューマンピクトグラム2.0" "http://pictogram2.com/" "/assets/images/card/frame/report.gif" "" "https://google.com" 0
     in
     cardView cardData
 
@@ -123,6 +114,14 @@ cardView cardData =
 
             else
                 elm
+
+        cardId =
+            case cardData.cardId of
+                Just id ->
+                    CardId.toString id
+
+                Nothing ->
+                    ""
     in
     div [ class "skill-card" ]
         [ div [ class "wrapper" ]
@@ -153,7 +152,7 @@ cardView cardData =
                     , div [ class "description" ] [ text (labelData.description ++ cardData.description) ]
                     ]
                 , div [ class "bottomContent" ]
-                    [ div [ class "cardId" ] [ text cardData.cardId ]
+                    [ div [ class "cardId" ] [ text cardId ]
                     , illustedBy cardData
                     ]
                 ]
@@ -226,7 +225,7 @@ cardDecodeFromString s =
 cardDecoder : Decoder CardData
 cardDecoder =
     D.succeed CardData
-        |> Json.Decode.Pipeline.custom (D.index 0 D.string)
+        |> Json.Decode.Pipeline.custom (D.index 0 CardId.decoder)
         |> Json.Decode.Pipeline.custom (D.index 1 D.string)
         |> Json.Decode.Pipeline.custom (D.index 2 D.string)
         |> Json.Decode.Pipeline.custom (D.index 3 D.string)
@@ -247,57 +246,3 @@ cardDecoder =
         |> Json.Decode.Pipeline.custom (D.index 18 D.string)
         |> Json.Decode.Pipeline.custom (D.index 19 D.string)
         |> Json.Decode.Pipeline.custom (D.index 20 GSAPI.decoderIntFromString)
-
-
-tagsDecoder : Decoder (List Tag)
-tagsDecoder =
-    D.map tagsParser string
-
-
-tagsParser : String -> List Tag
-tagsParser s =
-    let
-        list =
-            String.split "," s
-    in
-    List.map (\str -> tagParser str) list
-
-
-tagDecoder : Decoder Tag
-tagDecoder =
-    D.map tagParser string
-
-
-tagParser : String -> Tag
-tagParser s =
-    let
-        list =
-            String.split ":" s
-
-        name =
-            case List.head list of
-                Just a ->
-                    a
-
-                _ ->
-                    s
-
-        value =
-            case List.tail list of
-                Just t ->
-                    case List.head t of
-                        Just a ->
-                            case String.toInt a of
-                                Just n ->
-                                    n
-
-                                _ ->
-                                    0
-
-                        _ ->
-                            0
-
-                _ ->
-                    0
-    in
-    Tag name value

```

### Tags.elm

アドバイスでいただいたソースをそのまま適用したもの。
0のときにネストしていた部分が横並びになって、可読性が増している。

```elm:Models/Tags.elm
module Models.Tag exposing (Tag, tagsDecoder)

import Json.Decode as D exposing (Decoder)


type alias Tag =
    { name : String
    , level : Int
    }


fromString : String -> Maybe Tag
fromString str =
    case String.split ":" str of
        [ "" ] ->
            Nothing

        [] ->
            Nothing

        name :: level :: _ ->
            Just <| Tag name (String.toInt level |> Maybe.withDefault 0)

        name :: _ ->
            Just <| Tag name 0


tagsDecoder : Decoder (List Tag)
tagsDecoder =
    D.map
        (\str ->
            String.split "," str
                |> List.filterMap fromString
        )
        D.string

```

### CardId.elm

[値が制約を満たしていることを型で保証する][*1]を参考に、CardIdを作る。

```elm:Models/CardId.elm
module Models.CardId exposing (CardId, decoder, fromString, toString)

import Json.Decode as D exposing (Decoder)


type CardId
    = CardId String


{-| `CardId` 型の値を作成する唯一の方法

    fromString ""
    --> Nothing

    fromString "B-001"
    --> Just (CardId "B-001")

-}
fromString : String -> Maybe CardId
fromString s =
    if String.length s /= 0 then
        Just (CardId s)

    else
        Nothing


{-| `CardId` 型の値を文字列に変換する唯一の方法
-}
toString : CardId -> String
toString (CardId s) =
    s


decoder : Decoder (Maybe CardId)
decoder =
    D.map fromString D.string
```

## 2019/4/30 追加修正

コメントで指摘いただいたので修正

* `CardId.fromString`のような形で呼び出す場合には`exposing`不要
* 不要な`exposing`は、例えば`fromString`を使った場合に、何の`fromString`か分からなくなるので混乱のもと

```diff:Models/Card.elm
-import Models.CardId as CardId exposing (CardId, decoder, fromString, toString)
+import Models.CardId as CardId exposing (CardId)
```


## 参考

[値が制約を満たしていることを型で保証する][*1]

[*1]:https://qiita.com/arowM/items/0106f2b66a6a446bad38