Создание экземпляра Read в Haskell
У меня есть тип данных
data Time = Time {hour :: Int,
minute :: Int
}
для которого я определил экземпляр Show как
instance Show Time where
show (Time hour minute) = (if hour > 10
then (show hour)
else ("0" ++ show hour))
++ ":" ++
(if minute > 10
then (show minute)
else ("0" ++ show minute))
который печатает раз в формате 07:09
,
Теперь должна быть симметрия между Show
а также Read
Итак, после прочтения (но не истинного (я думаю) понимания) этого и этого, а также прочтения документации, я придумал следующий код:
instance Read Time where
readsPrec _ input =
let hourPart = takeWhile (/= ':')
minutePart = tail . dropWhile (/= ':')
in (\str -> [(newTime
(read (hourPart str) :: Int)
(read (minutePart str) :: Int), "")]) input
Это работает, но ""
часть заставляет это казаться неправильным. Итак, мой вопрос в конечном итоге:
Может ли кто-нибудь объяснить мне правильный способ реализации чтения, чтобы разобрать "07:09"
в newTime 7 9
и / или показать мне?
2 ответа
Я буду использовать isDigit
и держите ваше определение времени.
import Data.Char (isDigit)
data Time = Time {hour :: Int,
minute :: Int
}
Вы использовали, но не определили newTime
так что я сам написал так, мой код компилируется!
newTime :: Int -> Int -> Time
newTime h m | between 0 23 h && between 0 59 m = Time h m
| otherwise = error "newTime: hours must be in range 0-23 and minutes 0-59"
where between low high val = low <= val && val <= high
Во-первых, ваш экземпляр шоу немного не прав, потому что show $ Time 10 10
дает "010:010"
instance Show Time where
show (Time hour minute) = (if hour > 9 -- oops
then (show hour)
else ("0" ++ show hour))
++ ":" ++
(if minute > 9 -- oops
then (show minute)
else ("0" ++ show minute))
Давайте посмотрим на readsPrec
:
*Main> :i readsPrec
class Read a where
readsPrec :: Int -> ReadS a
...
-- Defined in GHC.Read
*Main> :i ReadS
type ReadS a = String -> [(a, String)]
-- Defined in Text.ParserCombinators.ReadP
Это парсер - он должен возвращать несопоставленную оставшуюся строку, а не просто ""
так что вы правы, что ""
неправильно:
*Main> read "03:22" :: Time
03:22
*Main> read "[23:34,23:12,03:22]" :: [Time]
*** Exception: Prelude.read: no parse
Он не может разобрать, потому что вы выбросили ,23:12,03:22]
в первом чтении.
Давайте сделаем рефакторинг, чтобы немного покушать входные данные по мере продвижения:
instance Read Time where
readsPrec _ input =
let (hours,rest1) = span isDigit input
hour = read hours :: Int
(c:rest2) = rest1
(mins,rest3) = splitAt 2 rest2
minute = read mins :: Int
in
if c==':' && all isDigit mins && length mins == 2 then -- it looks valid
[(newTime hour minute,rest3)]
else [] -- don't give any parse if it was invalid
Дает например
Main> read "[23:34,23:12,03:22]" :: [Time]
[23:34,23:12,03:22]
*Main> read "34:76" :: Time
*** Exception: Prelude.read: no parse
Однако он допускает "3:45" и интерпретирует его как "03:45". Я не уверен, что это хорошая идея, поэтому, возможно, мы могли бы добавить еще один тест length hours == 2
,
Я откажусь от всего этого, если мы будем делать это таким образом, поэтому, возможно, я бы предпочел:
instance Read Time where
readsPrec _ (h1:h2:':':m1:m2:therest) =
let hour = read [h1,h2] :: Int -- lazily doesn't get evaluated unless valid
minute = read [m1,m2] :: Int
in
if all isDigit [h1,h2,m1,m2] then -- it looks valid
[(newTime hour minute,therest)]
else [] -- don't give any parse if it was invalid
readsPrec _ _ = [] -- don't give any parse if it was invalid
Что на самом деле кажется мне чище и проще.
На этот раз это не позволяет "3:45"
:
*Main> read "3:40" :: Time
*** Exception: Prelude.read: no parse
*Main> read "03:40" :: Time
03:40
*Main> read "[03:40,02:10]" :: [Time]
[03:40,02:10]
Если вход в readsPrec
это строка, которая содержит некоторые другие символы после правильного представления Time
эти другие символы должны быть возвращены как второй элемент кортежа.
Так что для строки 12:34 bla
результат должен быть [(newTime 12 34, " bla")]
, Ваша реализация вызовет ошибку для этого ввода. Это означает, что что-то вроде read "[12:34]" :: [Time]
потерпит неудачу, потому что это вызовет Time
"s readsPrec
с "12:34]"
в качестве аргумента (потому что readList
будет потреблять [
затем позвоните readsPrec
с оставшейся строкой, а затем проверьте, что оставшаяся строка возвращается readsPrec
либо ]
или запятая, за которой следуют дополнительные элементы).
Чтобы исправить ваши readsPrec
вы должны переименовать minutePart
что-то вроде afterColon
а затем разделить это на фактическую минутную часть (с takeWhile isDigit
например) и что будет после минутной части. Затем материал, который появился после минутной части, должен быть возвращен как второй элемент кортежа.