Making a Read instance in Haskell

后端 未结 2 1847
梦如初夏
梦如初夏 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]
    
    0 讨论(0)
  • 2020-12-28 17:13

    If the input to readsPrec is a string that contains some other characters after a valid representation of a Time, those other characters should be returned as the second element of the tuple.

    So for the string 12:34 bla, the result should be [(newTime 12 34, " bla")]. Your implementation would cause an error for that input. This means that something like read "[12:34]" :: [Time] would fail because it would call Time's readsPrec with "12:34]" as the argument (because readList would consume the [, then call readsPrec with the remaining string, and then check that the remaining string returned by readsPrec is either ] or a comma followed by more elements).

    To fix your readsPrec you should rename minutePart to something like afterColon and then split that into the actual minute part (with takeWhile isDigit for example) and whatever comes after the minute part. Then the stuff that came after the minute part should be returned as the second element of the tuple.

    0 讨论(0)
提交回复
热议问题