在Haskell中创建一个Read实例

22

我有一个数据类型

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 输出时间。

现在,ShowRead 应该对称,所以在阅读(但不完全(我想)理解)这篇文章这篇文章,以及阅读文档后,我得出了以下代码:

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个回答

24

我会使用 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]

1
感谢您指出 show 函数中的一位偏移错误。这段新代码更有意义,如果不是我忘记提到“智能构造函数” newTime 已经进行了有效性检查,它就更正确了。 - Magnap
@Magnap 我已经将不必要的检查删除并使用了 newTime。 - AndrewC
@Magnap 在调用Int之前,我仍然需要检查是否有数字。 - AndrewC
谢谢。然而,当在列表中出现像“9:3”这样的时间时,它会失败,但是单独解析它可以正确地解析。总体而言,它更加正确。这也是一个很好的解析模板。 - Magnap
@Magnap 实际上,我认为它不应该解析 9:3,所以我进行了编辑。原因是 splitAt 2 "3" 给出的结果是 ("3",""),因此在数据不足的情况下会悄悄地成功,而如果它在列表中,例如 splitAt 2 "3]" 给出的结果是 ("3]",""),并且 "3]" 无法被读取。 - AndrewC
当然,后面的代码也更简单(并且失败得更“正确”),因此强制执行“规范”比前者更好。感谢您在这个问题上的指导。 - Magnap

5
如果传给readsPrec的输入是一个包含有效Time表示后其他字符的字符串,那么这些其他字符应该作为元组的第二个元素返回。
对于字符串12:34 bla,结果应该是[(newTime 12 34, " bla")]。您的实现会导致该输入出错。这意味着像read "[12:34]" :: [Time]这样的语句会失败,因为它会将TimereadsPrec作为参数调用"12:34]"(因为readList将消耗[,然后使用剩下的字符串调用readsPrec,然后检查readsPrec返回的剩余字符串是]还是逗号后跟更多元素)。
要修复您的readsPrec,您应该将minutePart重命名为afterColon,然后将其拆分为实际的分钟部分(例如使用takeWhile isDigit)和分钟部分之后的任何内容。然后将跟在分钟部分之后的内容作为元组的第二个元素返回。

非常感谢。在将let更改为(为了可读性而加括号){hourPart = takeWhile (/= ':') input; afterColon = tail . dropWhile (/= ':') $ input; minutePart = takeWhile isDigit afterColon; rest = dropWhile isDigit afterColon;}之后,它的工作非常出色。 - Magnap

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接