Making a Read instance in Haskell

后端 未结 2 1860
梦如初夏
梦如初夏 2020-12-28 16:38

I have a data type

data Time = Time {hour :: Int,
                  minute :: Int
                 }

for which i have defined the instance

2条回答
  •  粉色の甜心
    2020-12-28 17:01

    I'll use isDigit and keep your definition of Time.

    import Data.Char (isDigit)
    
    data Time = Time {hour :: Int,
                      minute :: Int
                     }
    

    You used but didn't define newTime, so I wrote one myself so my code compiles!

    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
    

    Firstly, your show instance is a little wrong because show $ Time 10 10 gives "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))
    

    Let's have a look at 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
    

    That's a parser - it should return the unmatched remaining string instead of just "", so you're right that the "" is wrong:

    *Main> read "03:22" :: Time
    03:22
    *Main> read "[23:34,23:12,03:22]" :: [Time]
    *** Exception: Prelude.read: no parse
    

    It can't parse it because you threw away the ,23:12,03:22] in the first read.

    Let's refactor that a bit to eat the input as we go along:

    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
    

    Gives for example

    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
    

    It does, however, allow "3:45" and interprets it as "03:45". I'm not sure that's a good idea, so perhaps we could add another test length hours == 2.


    I'm going off all this split and span stuff if we're doing it this way, so maybe I'd prefer:

    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
    

    Which actually seems cleaner and simpler to me.

    This time it doesn't allow "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]
    

提交回复
热议问题