Help us understand the problem. What is going on with this article?

Elmで読み取りのみ可能なレコードを作りたい

読み取り可能なレコードを作りたい

気持ちとしては、そのデータ構造にレコードとして.hogehogeってアクセスしたいけど、不正な値へのマッピングを許したくないのでタイトルのような気持ちになった

問題の実装

type alias MyTime = { hours : Int, minutes : Int }

という型を作ったのだが、いけないことができてしまう

aho : MyTime 
aho = MyTime 0 1234567 -- 60分以上になってるのや、24時間をこえるものは作れないようにしたい

コンストラクタを作って、それを公開しないようにする

結局こうなった。

module MyTime exposing (MyTime, MyValidTime, fromMyTime, myTime)


type MyValidTime
    = MyValidTime MyTime


type alias MyTime =
    { hours : Int
    , minutes : Int
    }


fromMyTime : MyTime -> Maybe MyValidTime
fromMyTime { hours, minutes } =
    let
        h =
            hours + (minutes // 60)

        m =
            modBy 60 minutes
    in
    if h >= 24 then
        Nothing

    else
        Just << MyValidTime <| MyTime h m


myTime : MyValidTime -> MyTime
myTime (MyValidTime t) =
    t

この実装のありがたみ

これの何がうれしいかというと

module Foo exposing (..)

import MyTime exposing (MyTime, MyValidTime, myTime, fromMyTime)

a : MyTime
a = MyTime 0 123


{- MyValidTimeコンストラクタがないってエラーになる
   a : MyValidTime
   a = MyValidTime (MyTime 0 1234567) -}


{- fromMyTime関数を通さないとMyValidTimeが作れないので、
   MyValidTimeは必ずfromMyTime関数を使うことになる -}
validA : Maybe MyValidTime
validA = fromMyTime a -- Just (MyValidTime { hours = 2, minutes = 3 }) になる



{- MyTimeという名前が付いてても別にminutesはIntなので、
   不正な値が入っているかどうかはMyTime型からわからない -}
aMinutes : Int
aMinutes = a.minutes 


{- しかし、MyValidTimeはfromMyTimeからしか作ることができず、
   その実装からminutesが確実に60未満になることがわかる -}
validAMinutes : Int
validAMinutes = validA
    |> Maybe.map (myTime >> .minutes)
    |> Maybe.withDefault 0


-- ユーザが勝手にこういう実装をしてしまうことも
add100minutes : MyTime -> MyTime
add100minutes v =
    { v | minutes = v.minutes + 100 }


-- MyValidTime型ならユーザがどんな実装をしても24時間超えたり、分が60分を絶対に超えない
validAdd100minutes : MyValidTime -> Maybe MyValidTime
validAdd100minutes v =
    Debug.todo "破れるもんならやってみろ!"

いちいちmyTimeっていう変換をかける手間は惜しいけど、これで一応読み取り可能なレコードができた。

MyValidTimeのデフォルトのコンストラクタを隠蔽して、fromMyTimeという別のコンストラクタ関数を作成することによってMyValidTime型にfromMyTimeの実装である必ず24時間以内であって、minutesも60未満であることが保証されているということが型からわかるようになる。それはコンストラクタによって与えられてる制約で、その関数が一つしかないからである。(もしデフォルトコンストラクタもexportしていたらライブラリユーザが好き勝手中身を弄れることになるので、型からわからなくなる)。

そして、そのMyValidTimeというデータの中身を読み取るためにmyTime関数をつかい、中身をレコードの形式で取り出すことができるようになったので、これで読み取り可能なレコードとしての機能が出来上がった。

【追記】 もうちょいレコードっぽく!

命名をもうちょい考え直すことにした。MyValidTimeもじゅーるは内部表現で使ってたMyTimeといったレコード型をエクスポートせずに、レコードコンストラクタっぽい関数を用意してあげることにした。そして、それぞれのレコードにアクセスするような関数を実装すると

module MyTime exposing (MyValidTime, MyValidTime, hour, minute, toRecord)


type MyValidTime
    = MyValidTime Int Int


myValidTime : Int -> Int -> Maybe MyValidTime
myValidTime hours minutes =
    let
        h =
            hours + (minutes // 60)

        m =
            modBy 60 minutes
    in
    if h >= 24 then
        Nothing

    else
        Just (MyValidTime h m)


hours : MyValidTime -> Int
hours (MyValidTime h _) = h


minutes : MyValidTime -> Int
minutes (MyValidTime _ m) = m


toRecord :: MyValidTime -> { hour : Int, minute : Int }
toRecord (MyValidTime h m =
    { hour = h
    , minute = m
    }

結局、MyValidTime : Int -> Int -> MyValidTimeというコンストラクタの代わりにラップしたmyValidTime : Int -> Int -> MyValidTimeといったコンストラクタを作成し、それぞれのアクセサを明示的に実装しただけになってしまった。いや、本来こうあるべきだったのだろう...読み取り可能なレコードがやっと作れた。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした