Подтвердить что ты не робот

Создание экземпляра 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, поэтому после чтения (но не по-настоящему (я думаю) понимания) this и this и прочитав документацию, я придумал следующий код:

instance Read Time where
  readsPrec _ input =
    let hourPart = takeWhile (/= ':')
        minutePart = tail . dropWhile (/= ':')
    in (\str -> [(newTime
                  (read (hourPart str) :: Int)
                  (read (minutePart str) :: Int), "")]) input

Это работает, но часть "" делает это неправильным. Итак, мой вопрос заканчивается:

Может ли кто-нибудь объяснить мне правильный способ реализовать Read для синтаксического анализа "07:09" в newTime 7 9 и/или показать мне?

4b9b3361

Ответ 1

Я использую 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 немного неверен, потому что 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]

Ответ 2

Если вход в readsPrec является строкой, которая содержит некоторые другие символы после допустимого представления Time, эти другие символы должны быть возвращены как второй элемент кортежа.

Итак, для строки 12:34 bla результат должен быть [(newTime 12 34, " bla")]. Ваша реализация вызовет ошибку для этого ввода. Это означает, что что-то вроде read "[12:34]" :: [Time] потерпит неудачу, потому что он будет вызывать Time readsPrec с "12:34]" в качестве аргумента (потому что readList будет потреблять [, а затем вызывает readsPrec с оставшейся строкой и затем проверьте, что оставшаяся строка, возвращаемая readsPrec, либо ], либо запятая, за которой следуют другие элементы).

Чтобы исправить ваш readsPrec, вы должны переименовать minutePart в нечто вроде afterColon, а затем разделите его на фактическую минутную часть (например, с помощью takeWhile isDigit) и все, что приходит после минутной части. Затем материал, который пришел после минутной части, должен быть возвращен как второй элемент кортежа.