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]