20

我有一个数据类型

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现在, and之间应该是对称的Read,所以在阅读(但不是真正(我认为)理解)thisthis并阅读文档之后,我想出了以下代码:

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

4

2 回答 2

22

我将使用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

那是一个解析器 - 它应该返回不匹配的剩余字符串而不是 just "",所以你是对的,这""是错误的:

*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]
于 2012-12-22T22:03:00.130 回答
4

如果输入readsPrec是在 a 的有效表示之后包含一些其他字符的字符串Time,则这些其他字符应作为元组的第二个元素返回。

所以对于字符串12:34 bla,结果应该是[(newTime 12 34, " bla")]。您的实现会导致该输入出错。这意味着类似的东西read "[12:34]" :: [Time]会失败,因为它会调用Time's作为参数(因为会消耗,然后用剩余的字符串调用readsPrec,然后检查返回的剩余字符串是不是或者是逗号,后跟更多元素)。"12:34]"readList[readsPrecreadsPrec]

要解决您的问题readsPrec,您应该重命名minutePart为类似的名称afterColon,然后将其拆分为实际的分钟部分(takeWhile isDigit例如)以及分钟部分之后的任何内容。然后,分钟部分之后的内容应该作为元组的第二个元素返回。

于 2012-12-22T21:41:05.803 回答