LoginSignup
8
0

More than 3 years have passed since last update.

[Elm] より仕様にあった型定義をしてみる(CustumType, OpaqueTypeの応用)

Last updated at Posted at 2019-09-25

前回勉強してQiitaに投稿したCustomTypeとOpaqueTypeが実開発で活きたので、今回はその話を書きます!

題材となるものの仕様

図のような、年代別の出身国の人数をまとめたテーブルを表示するとする。

表を構成するデータは、以下の形でサーバから取得される。データの集計は全てサーバが行っており、フロント(Elm)はサーバから集計結果を受け取り表示するのみとする。

datas: [
    { age: "10代", country: "アメリカ", people_count:  6 },
    { age: "10代", country: "ブラジル", people_count: 13 },
    { age: "10代", country: "フランス", people_count:  7 },
    { age: "10代", country:     "", people_count:  2 },
    { age: "10代",                  people_count: 28 },
    { age: "20代", country: "オランダ", people_count: 10 },
    { age: "20代", country: "  日本", people_count:  4 },
    { age: "20代", country:     "", people_count:  1 },
    { age: "20代",                  people_count: 25 },
]

countryについて

サーバから受け取った生のデータは

  • countryというfield自体がない(その年代の「全体」を表すデータ)
  • countryが空文字(出身国が「未設定」であるデータ)
  • countryが国名を表す文字列

となっている。

elm-decode-pipelineのoptionalを使いデコード後に

  • "全体"(optionalのdefaultValueを利用)
  • ""(空文字)
  • "アメリカ", "ブラジル"...(任意の国名)

というように全てStringに変換する。

さらに空文字の場合は、「未設定」と表示するため変換する。

if country == "" then
    "未設定"
else
    country

リファクタ前のコードと問題点

countryの値は全てStringなので、最初Stringで定義してた。

type Data
    = Data
        { age : String
        , country : String
        , peopleCount : Int
        }

問題点1

  • 全部Stringだけど、「全体」「未設定」「任意の国名」の3パターンに分類できることに気づいた。
  • Stringという型定義は仕様に対してちょっと緩く、最適とは言えない。

=> これはCustumTypeの出番!?

問題点2

  • いろんなところでcountry == ""country == "全体" といった比較表現が出てきてコードが煩雑になってきた。
  • countryの内部実装を考えなくても、countryを呼び出したら、「全体」「未設定」「任意の部署名」のどれかのStringが返ってくるようになってたら、使いやすくていいんだけどなぁ。

=> これはOpaqueTypeの出番!?

リファクタ後のコード

Country.elm

type Country
    = Country Kind

type Kind
    = Total
    | NotSet
    | HasName String

new : String -> Country
new value =
    if value == "全体" then
        Country Total

    else if value == "" then
        Country NotSet

    else
        Country (HasName value)

isTotal : Country -> Bool
isTotal (Country kind) =
    kind == Total

isNotSet : Country -> Bool
isNotSet (Country kind) =
    kind == NotSet

name : Country -> String
name (Country kind) =
    case kind of
        HasName name ->
            name

        Total ->
            ""

        NotSet ->
            ""

View.elm

"全体"と表示するか、"未設定"と表示するか、国名を表示するかはViewの責務なのでViewに以下のような条件分岐を持たせる。

if Country.isTotal data.country then
    "全体"
else if Country.isNotSet data.country then
    "未設定"
else
    Country.name data.country

問題解決1

  • 「全体」「未設定」「任意の国名」の3パターンがあることをコードで表現でき、より仕様に合った型定義になった。
  • 型定義がより厳密になり安全になった。
  • コードの読み手がコードから仕様をくみ取りやすくなった。

問題解決2

  • 特に複雑なことを考えなくても、countryを呼び出せばパターン別に対応する文字列が返ってくるので使いやすくなった。
  • Countryは内部実装はCustomTypeで実装されているが、それを知らなくてもまるでStringのように扱えるようになっている。
  • isTotal, isNotSetといったAPIをCountryモジュールに持たせることで、data.countryの値を渡すだけで、実際にどんな文字列が入っているかを考えずに比較ができるようになった。

使う側の例1

div [] [text <| Country.toString data.country]

使う側の例2

-- data.country == "全体" みたいなのを書かなくてよくなった
if Country.isTotal data.country then
    --
else
    --

余談

今回は以下のような定義付けをした。

type Country
    = Country Kind

type Kind
    = Total
    | NotSet
    | HasName String

OpaqueTypeは必ず名前を同じにしなきゃいけない訳ではないが、名前を同じにする風習があるらしい。
また、CustumTypeも中の個々の型を公開しない(= Hoge(..)の形で公開しない)のであれば、内部の型を隠蔽できているのでOpaqueTypeになっていると言える。
今回のCountry

type Country
    = Total
    | NotSet
    | HasName String

と定義してCountryだけ公開すればOpaqueTypeになるが、定義が抽象的すぎてしまうので、あえてKindという要素を定義した。

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