2012-12-22 10 views
13

Ho un tipo di datiFare un'istanza Leggi in Haskell

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

per il quale ho definito l'istanza di Visualizza come

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)) 

che stampa gli orari in un formato di 07:09.

Ora, ci dovrebbe essere simmetria tra Show e Read, quindi dopo aver letto (ma non realmente (credo) la comprensione) this e this, e la lettura del documentation, sono venuto su con il seguente codice:

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

Questo funziona, ma la parte "" sembra sbagliata. Quindi la mia domanda finisce per essere:

Qualcuno può spiegarmi il modo corretto di implementare Leggi per analizzare "07:09" in newTime 7 9 e/o mostrarmi?

risposta

16

Userò isDigit e manterrò la definizione di Ora.

import Data.Char (isDigit) 

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

Si è utilizzato, ma non ha definito newTime, così ho scritto uno io così il mio codice compila!

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 

In primo luogo, l'istanza spettacolo è un po 'sbagliato, perché 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)) 

Diamo uno sguardo a 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 

Questo è un parser - dovrebbe restituire la stringa rimanente senza corrispondenza anziché solo "", in modo che tu abbia ragione che il "" è sbagliato:

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

Non può analizzare perché hai gettato via il ,23:12,03:22] nella prima lettura.

refactoring di lasciare che un po 'di mangiare l'ingresso come andiamo avanti:

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 

Dà ad esempio

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 

Esso, tuttavia, permettere "03:45" e lo interpreta come "3:45". Non sono sicuro che sia una buona idea, quindi forse potremmo aggiungere un altro test length hours == 2.


sto andando fuori tutta questa roba scissione e la durata se stiamo facendo in questo modo, così forse preferirei:

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 

che in realtà sembra più pulito e più semplice per me.

Questa volta non permette "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

Grazie per aver segnalato l'errore "off-one" nello show. Questo nuovo codice ha più senso, e sarebbe stato più corretto, se non fosse per il fatto (che ho dimenticato di menzionare), che il "costruttore intelligente" 'newTime' già esegue il controllo di validità. – Magnap

+0

@Magnap Ho modificato i controlli non necessari e ho usato newTime. – AndrewC

+0

@Magnap Ho ancora bisogno di controllare che abbia digits prima di chiamare 'read' su' Int's. – AndrewC

4

Se l'input per readsPrec è una stringa che contiene alcuni altri caratteri dopo una rappresentazione valida di uno Time, questi altri caratteri devono essere restituiti come il secondo elemento della tupla.

Quindi per la stringa 12:34 bla, il risultato dovrebbe essere [(newTime 12 34, " bla")]. L'implementazione causerebbe un errore per quell'input. Questo significa che qualcosa di simile read "[12:34]" :: [Time] fallirebbe perché avrebbe chiamato Time s' readsPrec con "12:34]" come argomento (perché readList sarebbe consumare il [, quindi chiamare readsPrec con la stringa rimanente, e quindi controllare che la stringa restante restituita da readsPrec è o ] o una virgola seguita da più elementi).

per risolvere il tuo readsPrec si dovrebbe rinominare minutePart a qualcosa come afterColon e poi dividere che nella parte reale minuto (con takeWhile isDigit per esempio) e ciò che viene dopo la parte minuto. Quindi le cose che arrivano dopo la parte del minuto devono essere restituite come il secondo elemento della tupla.

+0

Grazie mille. Dopo aver modificato la let a (parentesi per leggibilità) '{hourPart = takeWhile (/ = ':') input; afterColon = tail. dropWhile (/ = ':') $ input; minutePart = takeWhile isDigit afterColon; rest = dropWhile isDigit afterColon;} 'funziona meravigliosamente. – Magnap