% Práctico Haskell 4: Tipos de datos algebraicos
Ejercicios cortos
Definir los tipos y funciones siguientes en un archivo tipos.hs
.
Tipos enumerados
Cuando los distintos valores que debemos distinguir en un tipo son finitos, podemos enumerar todos los valores distintos para el tipo. Por ejemplo, podrı́amos representar los tı́tulos nobiliarios de algún paı́s (retrógrado) con el siguiente tipo:
data Titulo = Ducado | Marquesado | Condado | Vizcondado | Baronia
a) Definí el tipo Titulo
como descrito arriba.
b) Definí hombre :: Titulo -> String
que devuelve la forma masculina
del tı́tulo.
c) Definí la función dama
que devuelve la inflexión femenina.
Constructores con parámetros
En este ejercicio, introducimos dos conceptos: los sinónimos de tipos y tipos algebraicos cuyos constructores llevan parámetros. Los sinónimos de tipos nos permiten definir nombres de tipos nuevo a partir de tipos ya existentes:
-- Territorio y Nombre son sinonimos de tipo.
type Territorio = String
type Nombre = String
cba , bsas :: Territorio
cba = "Cordoba"
bsas = "Buenos Aires"
alicia , bob :: Nombre
alicia = "Alicia"
bob = "Bob"
Los tipos algebraicos tienen constructores que llevan parámetros. Esos parámetros nos permiten agregar información; por ejemplo, en este ejercicio además de distinguir el rango de cada persona, representamos datos pertinentes a cada tipo de persona:
-- Persona es un tipo algebraico
data Persona = Rey -- constructor sin parametro
| Noble Titulo Territorio -- constructor con dos parametros
| Caballero Nombre -- constructor con un parametro
| Aldeano Nombre -- constructor con un parametro
a) Definí los tipos Territorio
, Nombre
y Persona
como descrito
arriba.
b) Definí la función tratamiento :: Persona -> String
que dada una persona devuelve la forma en que se lo menciona
en la corte. Esto es, al rey se lo nombra “Su majestad el rey”,
a un noble se lo nombra “El
Analizador de Logs
¡Algo terrible pasó con nuestros servidores!
- Para los ejercicios que siguen, editá tus respuestas en un archivo llamado
LogAnalysis.hs
- Vas a necesitar los archivos:
Log.hs
,error.log
,sample.log
Parsear archivos de depuración
No sabemos bien lo que pasó, pero hemos logrado encontrar el archivo
de depuración error.log
. Parece que está constituido de un mensaje
de depuración en cada línea. Cada línea empieza con un carácter que
indica el tipo de mensaje que representa:
- ’I’ para mensajes informativos,
- ’W’ para advertencias (warnings)
- ’E’ para errores.
Además, las líneas de mensajes de error tienen un valor entero que indica la gravedad del error, con 1 siendo el tipo de error que podés dejar para el próximo verano, y 100 siendo una falla épica y total. Luego, todos los tipos de mensajes de depuración tienen una fecha (bajo la forma de un número entero) seguida del contenido textual hasta el fin de la línea.
Este es un extracto del archivo de depuración, que incluye un mensaje informativo seguido de un mensaje de error de nivel 2:
I 147 mice in the air, I’m afraid, but you might catch a bat, and
E 2 148 #56k istereadeat lo d200ff] BOOTMEM
Está todo un poco confuso; claramente necesitamos un programa para ordenar este lío. Hemos encontrado algunos tipos de datos que capturan la estructura del formato de archivo de depuración:
data MessageType = Info
| Warning
| Error Int
deriving (Show, Eq)
type TimeStamp = Int
data LogMessage = LogMessage MessageType TimeStamp String
deriving (Show, Eq)
(Las anotaciones deriving (Show, Eq)
permiten que estos
tipos sean impresos en una sesión GHCi y permiten comparaciones
de igualdad.)
Te proveemos un módulo Log.hs
que contiene estas declaraciones
de tipos de datos, con algunas funciones útiles. Descargá
Log.hs
y ponelo en la misma carpeta donde vas a poner tu
propio módulo LogAnalysis.hs
. Las primeras líneas de
LogAnalysis.hs
deberían ser las siguientes:
{-# OPTIONS_GHC -Wall #-}
module LogAnalysis where
import Log
Sirven para declarar que el archivo es un módulo llamado
LogAnalysis
, e importa el módulo almacenado en Log.hs
para
que puedas usar sus tipos y funciones.
Ejercicio 1
El primer paso consiste en encontrar como analizar un mensaje
individual. Puede ser que el archivo esté corrupto y
que algunas líneas individuales estén inutilizables.
Entonces, no podemos estar seguros
que una línea de la entrada va a ser un LogMessage
válido.
Por lo cual vamos a definir un tipo (proveido en Log.hs
) que
permita la posibilidad de una falla:
data MaybeLogMessage = ValidLM LogMessage
| InvalidLM String
deriving (Show, Eq)
Como podés ver, un MaybeLogMessage
contiene o un LogMessage
válido,
o una String
sin formato.
Definí una función:
parseMessage :: String -> MaybeLogMessage
que parsea una línea individual del archivo de depuración.
Es mucho más fácil definir esta función
empezando por convertir la línea en una lista de palabras,
usando la función words
, y luego pensar en los 3 casos
correspondientes a reconocer un mensaje de tipo Info
, Warning
y Error
.
Por ello es recomendable definir parseMessage
como sigue:
parseMessage s =
case words s of
... -> ...
... -> ...
... -> ...
_ -> InvalidLM s
En Log.hs
está definida la función readInt
para procesar los valores enteros representados como String
.
Por ejemplo:
parseMessage "E 2 562 help help"
== ValidLM (LogMessage (Error 2) 562 "help help")
parseMessage "I 29 la la la"
== ValidLM (LogMessage Info 29 "la la la")
parseMessage "This is not in the right format"
== InvalidLM "This is not in the right format"
Ejercicio 2
No es muy difícil de hacer funcionar parseMessage
sobre todas las líneas
de un archivo. Pero, hacerlo produciría un [MaybeLogMessage]
,
mientras en realidad queremos solo un [LogMessage]
para seguir procesando
más. Así que tiremos los mensajes inválidos.
Definí por recursión una función:
validMessagesOnly :: [MaybeLogMessage] -> [LogMessage]
que descarta los mensajes inválidos.
Ejercicio 3
Ahora, podemos juntar los pedazos para definir
parse :: String -> [LogMessage]
que analiza un archivo de depuración entero y devuelve
los contenidos como una lista de LogMessages
.
Acá es recomendable empezar por partir el archivo en líneas
usando la función lines
, y definir una función local go
recursiva que se encargue de aplicar la función parseMessage
a cada línea.
Para probar tu función, usá la función testParse
proveida en el módulo
Log
, dándole como parámetro tu función parse
, la cantidad de líneas
que hay que parsear, y el archivo de depuración de donde hay que parsear
(que debería también estar en la misma carpeta que tu propio módulo).
Por ejemplo, después de cargar tu módulo en GHCi, tipeá algo como lo
siguiente:
testParse parse 10 "error.log"
Deberías ver lo siguiente:
LogMessage Info 5053 "pci_id: con ing!"
LogMessage Info 4681 "ehci 0xf43d000:15: regista14: [0xbffff 0xfed nosabled 00-02] Zonseres: brips byted nored)"
LogMessage Warning 3654 "e8] PGTT ASF! 00f00000003.2: 0x000 - 0000: 00009dbfffec00000: Pround/f1743colled"
LogMessage Info 4076 "verse.'"
LogMessage Info 4764 "He trusts to you to set them free,"
LogMessage Info 858 "your pocket?' he went on, turning to Alice."
LogMessage Info 898 "would be offended again."
LogMessage Info 3753 "pci 0x18fff steresocared, overne: 0000 (le wailan0: ressio0/derveld fory: alinpu-all)"
LogMessage Info 790 "those long words, and, what's more, I don't believe you do either!' And"
LogMessage Info 3899 "hastily."
Poner los mensajes en orden
Lamentablemente, como los mensajes de error fueron generados por servidores fallando en distintas ubicaciones del mundo (una tormenta eléctrica, un disco duro fallido, y un programador aburrido e incompetente), los mensajes de error están horriblemente desordenados. Hasta que no ordenemos un poco, no habrá forma de entender qué pasó.
Ejercicio 4
Cualquier función de ordenamiento va a tener que comparar dos
LogMessages
para ver cuál debería venir primero. Pero, como acabamos
de crear el tipo LogMessage
, no hay forma para la computadora de saber
como compararlos. ¡Tenemos que escribir una función de comparación!
En general, comparar dos elementos para ordenamiento puede producir uno
de los tres resultados siguientes: menor-a (LT
), igual-a (EQ
) o mayor-a (GT
).
Haskell representa esta idea con el tipo de datos siguiente:
data Ordering = LT | EQ | GT
Ordering
es parte de Prelude
(el conjunto de cosas que se incluyen automáticamente cuando
compilás un programa en Haskell),
entonces su definición no aparece en Log.hs
y tampoco tiene que aparecer en tu código.
Definí una función
compareMsgs :: LogMessage -> LogMessage -> Ordering
que compara dos LogMessages
según su fecha.
Ahí van unos ejemplos:
compareMsgs (LogMessage Warning 153 "Not a speck of light is showing, so the danger must be growing...")
(LogMessage Info 208 "the Weighted Companion Cube cannot talk")
== LT
compareMsgs (LogMessage (Error 101) 2001 "My God! It’s full of stars!")
(LogMessage Info 2001 "Daisy, Daisy, give me your answer do.")
== EQ
Ejercicio 5
Ahora que expresaste cómo comparar mensajes, podés ordenar la lista. Definí una función
sortMessages :: [LogMessage] -> [LogMessage]
que ordena la lista de mensajes. ¡No implementes un algoritmo de ordenamiento!
Mejor fijate en el módulo Data.List
, hay una función muy práctica.
Autopsia del archivo de depuración
Ejercicio 6
Ahora que podemos ordenar los mensajes de depuración, la única cosa que queda para hacer es extraer la información relevante. Hemos decidido que “relevante” significa “errores con gravedad de al menos 50”.
Definí una función
whatWentWrong :: [LogMessage] -> [String]
que toma una lista no ordenada de LogMessages
, y devuelve una lista de los
mensajes correpondiendo a cualquier error con una gravedad de 50 o más,
ordenados por fecha. (Por supuesto, podés usar tus funciones de los ejercicios
anteriores para hacer el ordenamiento.)
Por ejemplo, suponé que nuestro archivo de depuración se parezca a esto:
I 6 Completed armadillo processing
I 1 Nothing to report
E 99 10 Flange failed!
I 4 Everything normal
I 11 Initiating self-destruct sequence
E 70 3 Way too many pickles
E 65 8 Bad pickle-flange interaction detected
W 5 Flange is due for a check-up
I 7 Out for lunch, back in two time steps
E 20 2 Too many pickles
I 9 Back from lunch
Este archivo es proveido como sample.log
. Hay cuatro errores, tres de las cuales
tienen una gravedad mayor a 50. La salida de whatWentWrong
sobre sample.log
debería ser:
[ "Way too many pickles"
, "Bad pickle-flange interaction detected"
, "Flange failed!"
]
Podés probar tu función whatWentWrong
con testWhatWentWrong
,
que también se encuentra en el módulo Log
. Por ejemplo:
testWhatWentWrong parse whatWentWrong "error.log"
Ejercicio 7
Mirá la salida de una ejecución de testWhatWentWrong
.
Para los dos archivos de depuración proveidos, algún aderezo parece
sobresalir (¡son distintos aderezos para distintos archivos!).
Podría ser más informativo ver al mismo tiempo las advertencias y los
errores que mencionan ese aderezo en particular.
Definí una función:
messagesAbout :: String -> [LogMessage] -> [LogMessage]
que filtra una lista de LogMessages
para solo incluir los mensajes que
contengan la string proveida. Hacé que tu función sea insensible a las
mayúsculas, para que “relish
” encuentre “Relishsign detected!!
”.
Seguramente encontrarás en los módulos
Data.Char
y Data.List
algunas funciones útiles.
Ejercicio 8
Ahora queremos ver una salida combinada, con al mismo tiempo todos los mensajes de alta severidad y todos los mensajes que contengan alguna palabra-clave. Escribí una función:
whatWentWrongEnhanced :: String -> [LogMessage] -> [String]
esto genera una lista que incluye al mismo tiempo todos los errores graves y todos los mensajes que contengan la string proveida.
Observá que probablemente vas a crear funciones nuevas, que pueden servir para refactorizar código que escribiste antes, logrando así no repetirte demasiado.
Para probar tu función, ejecutá algo así en GHCi:
LogAnalysis> testWhatWentWrong parse (whatWentWrongEnhanced "relish") "error.log"
(¿Por qué funciona pasarle solo un parámetro a whatWentWrongEnhanced
?)