parsing haskell typeclass symmetry

parsing - Haciendo una instancia de lectura en Haskell



typeclass symmetry (2)

Tengo un tipo de datos

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

para lo cual he definido la instancia de Mostrar como siendo

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

que imprime tiempos en un formato de 07:09 .

Ahora, debe haber simetría entre Show y Read , así que después de leer (pero no realmente (creo) entender this y this , y leer la documentation , he encontrado el siguiente código:

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

Esto funciona, pero la parte "" hace parecer incorrecto. Entonces mi pregunta termina siendo:

¿Puede alguien explicarme la forma correcta de implementar Leer para analizar "07:09" en newTime 7 9 y / o mostrarme?


Si la entrada a readsPrec es una cadena que contiene algunos otros caracteres después de una representación válida de un Time , esos otros caracteres deben devolverse como el segundo elemento de la tupla.

Entonces, para la cadena 12:34 bla , el resultado debería ser [(newTime 12 34, " bla")] . Su implementación causaría un error para esa entrada. Esto significa que algo así como read "[12:34]" :: [Time] fallaría porque llamaría a readsPrec Time con "12:34]" como el argumento (porque readList consumiría [ , luego llamaría a readsPrec con la cadena restante, y luego verifique que la cadena restante devuelta por readsPrec sea ] o una coma seguida de más elementos).

Para arreglar su readsPrec , debe cambiar el nombre de minutePart a algo como afterColon y luego dividirlo en la parte de minutos real (con takeWhile isDigit por ejemplo) y lo que venga después de la parte de minutos. Luego, las cosas que vinieron después de la parte del minuto deberían devolverse como el segundo elemento de la tupla.


isDigit y mantendré tu definición de Tiempo.

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

newTime pero no newTime , ¡así que escribí uno para que newTime mi código!

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

En primer lugar, su instancia de show es un poco incorrecta porque show $ Time 10 10 da "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))

Echemos un vistazo 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

Eso es un analizador: debería devolver la cadena restante sin emparejar en lugar de solo "" , así que tienes razón en que "" es incorrecto:

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

No puede analizarlo porque tiraste el ,23:12,03:22] en la primera lectura.

Vamos a refactorizar un poco para comer la entrada a medida que avanzamos:

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

Da por ejemplo

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

Sin embargo, sí permite "3:45" y lo interpreta como "03:45". No estoy seguro de que sea una buena idea, así que quizás podríamos agregar otra prueba de length hours == 2 .

Me estoy yendo de todo esto si lo hacemos de esta manera, tal vez prefiera:

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

Lo que en realidad me parece más limpio y sencillo.

Esta vez no permite "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]