読み取り可能なレコードを作りたい
気持ちとしては、そのデータ構造にレコードとして.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
といったコンストラクタを作成し、それぞれのアクセサを明示的に実装しただけになってしまった。いや、本来こうあるべきだったのだろう...読み取り可能なレコードがやっと作れた。