Página de Guillaume Hoffmann

View on GitHub

% 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 de ", a un caballero "Sir " y a un aldeano simplemente con su nombre. c) Agreguemos el tipo `data Genero = Hombre | Mujer` a nuestro módulo. ¿Cómo modificar el tipo `Persona` para poder representar personas de distintos géneros *sin agregarle más constructores*? Realizá esta modificación y vuelva a programar la función `tratamiento` de forma tal de respetar el género de la persona al nombrarla.

Analizador de Logs

¡Algo terrible pasó con nuestros servidores!

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:

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