Tracks
/
Elm
Elm
/
Ejercicios
/
La prestigiosa pizza de Paola
La prestigiosa pizza de Paola

La prestigiosa pizza de Paola

Ejercicio de aprendizaje

Introducción

Parseo

El parseo es el proceso de transformar texto en datos con significado. Las funciones del paquete String son limitadas en ese sentido y las expresiones regulares son difíciles de usar, así que la forma idiomática de parsear en Elm es con el paquete elm/parser.

Veamos un ejemplo de cómo escribir un parser para (un subconjunto de) el lenguaje de programación LOLCODE. Considera el siguiente programa comentado:

HAI 1                 # program version 1
CAN HAS STDIO?        # import stdio
VISIBLE "HAI WORLD!"  # print "HAI WORLD!"
KTHXBYE               # end of program

Primero, escribamos un tipo de Elm para representar los comandos posibles:

type Command
    = Version Int
    | Import String
    | Print String

Nuestro programa LOLCODE se puede modelar entonces como un List Command. Empecemos con un parser para la línea HAI 1:

versionP : Parser Command
versionP =
    Parser.succeed Version
        |. Parser.keyword "HAI"
        |. Parser.spaces
        |= Parser.int

versionP se construye usando un pipeline de parsers. El operador (|.) significa «parsea el string pero descarta el resultado» y (|=) significa «parsea el string y conserva el resultado». En este ejemplo, Parser.keyword "HAI" consumirá un «HAI», pero ese resultado se ignora. Parser.spaces consumirá cualquier cantidad de caracteres ' ', '\n' y '\r', pero eso también se ignorará. Parser.int parseará un entero, y el valor de ese entero (digamos 1) se pasará al Parser.succeed Version de más arriba, y el parser logrará devolver el valor Version 1.

Si alguno de los parsers del pipeline falla, devolverá un Parser.DeadEnd y todo el parser fallará. Un parser se puede ejecutar con Parser.run:

Parser.run versionP "HAI 1"
    --> Ok (Version 1)
Parser.run versionP "HAI 2"
    --> Ok (Version 2)
Parser.run versionP "BYE 2"
    --> Err [{ problem = ExpectingKeyword "HAI", col = 1, row = 1 }]
Parser.run versionP "HAI MOM"
    --> Err [{ problem = ExpectingInt, col = 5, row = 1 }]

Vamos a atacar la línea «CAN HAS STDIO?» con

importP : Parser Command
importP =
    Parser.succeed Import
        |. Parser.keyword "CAN HAS"
        |. Parser.spaces
        |= packageP
        |. Parser.symbol "?"

importP también es un pipeline de parsers, pero esta vez termina con Parser.symbol, que es adecuado para parsear símbolos como «?». El valor que queremos conservar (el nombre del paquete que se va a importar) lo parsea

packageP : Parser String
packageP =
    Parser.chompWhile Char.isAlphaNum
        |> Parser.getChompedString
        |> Parser.map String.toLower

Como no sabemos cuál será el nombre del paquete, no podemos usar Parser.keyword, así que en su lugar usamos Parser.chompWhile, que consumirá caracteres individuales siempre que cumplan una condición (aquí, ser un carácter alfanumérico). Esos caracteres consumidos los recoge después Parser.getChompedString, que devuelve un Parser String. En este caso, queremos normalizar los nombres de los paquetes, así que usamos Parser.map para modificar el string que contiene el parser y pasarlo a minúsculas.

Parser.run importP "CAN HAS STDIO?"
    --> Ok (Import "stdio")
Parser.run importP "CAN HAS StDiO?"
    --> Ok (Import "stdio")
Parser.run importP "CAN HAS STDIO"
    -->  Err [{ problem = ExpectingSymbol "?", col = 14, row = 1 }]

Ahora, VISIBLE "HAI WORLD!" se parsea con

printP : Parser Command
printP =
    Parser.succeed Print
        |. Parser.keyword "VISIBLE"
        |. Parser.spaces
        |. Parser.symbol "\""
        |= stringP
        |. Parser.symbol "\""

stringP : Parser String
stringP =
    Parser.chompWhile (\c -> c /= '"')
        |> Parser.getChompedString

Parser.run printP "VISIBLE \"HAI WORLD!\""
    --> Ok (Print "HAI WORLD!")

Ya tenemos un parser para cada comando. Claro que, en un programa general, no se puede predecir el orden de estos comandos, así que construimos un parser de comandos genérico

commandP : Parser Command
commandP =
    Parser.oneOf
        [ versionP
        , importP
        , printP
        ]

Parser.oneOf probará un parser a la vez, en el orden dado. Si un parser falla, se prueba el siguiente; pero si tiene éxito, se devuelve su resultado sin probar los parsers restantes. Si todos los parsers fallan, Parser.oneOf falla.

Parser.run commandP "HAI 2"
    --> Ok (Version 2)
Parser.run commandP "CAN HAS STDIO?"
    --> Ok (Import "stdio")
Parser.run commandP "VISIBLE \"HAI WORLD!\""
    --> Ok (Print "HAI WORLD!")
Parser.run commandP "O NOES"
    --> Err [ { problem = ExpectingKeyword "HAI", col = 1, row = 1 }
    --      , { problem = ExpectingKeyword "CAN HAS", col = 1, row = 1 }
    --      , { problem = ExpectingKeyword "VISIBLE", col = 1, row = 1 }
    --      ]

Fíjate en que cada intento fallido produce un Parser.DeadEnd que recoge Parser.oneOf.

Para parsear un programa completo, necesitamos parsear varios comandos en secuencia con

programP : Parser (List Command)
programP =
    Parser.sequence
        { start = ""
        , separator = "\n"
        , end = "KTHXBYE"
        , spaces = Parser.succeed ()
        , item = commandP
        , trailing = Parser.Optional
        }

Parser.sequence es un parser muy personalizable con varias opciones. start, separator y end reciben strings que separan los elementos de la secuencia; para parsear una lista de Elm usaríamos «[», «,» y «]». spaces da flexibilidad al recibir un parser que se encargará de los posibles espacios entre elementos y separadores; para parsear una lista de Elm usaríamos Parser.spaces, pero en este caso no aceptamos ningún espacio, así que usamos Parser.succeed (), que tiene éxito de inmediato sin consumir nada. item recibe el parser del elemento y, por último, trailing nos deja decidir si los separadores finales son Parser.Forbidden, Parser.Mandatory o Parser.Optional.

Parser.run programP "HAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Ok [Version 2, Import "stdio"]
Parser.run programP "KTHXBYE"
    --> Ok []
Parser.run programP "HAI 2\nCAN HAS STDIO?\n"
    --> Err [ { problem = ExpectingKeyword "HAI", col = 1, row = 3 }
    --      , { problem = ExpectingKeyword "CAN HAS", col = 1, row = 3 }
    --      , { problem = ExpectingKeyword "VISIBLE", col = 1, row = 3 }
    --      , { problem = Expecting "KTHXBYE", col = 1, row = 3 }
    --      ]

Cuando los parsers tienen éxito, dejan de consumir el string

Parser.run Parser.int "1"
    --> Ok 1
Parser.run Parser.int "1 BTW VISIBLE \"U SEE NOTHING\""
    --> Ok 1

Así que, si necesitamos asegurarnos de que parseamos todo el string, podemos usar Parser.end

fullProgramP : Parser (List Command)
fullProgramP =
    programP
        |. Parser.spaces
        |. Parser.end

Parser.run fullProgramP "HAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Ok [Version 2, Import "stdio"]
Parser.run fullProgramP "HAI 2\nCAN HAS STDIO?\nKTHXBYE\nBTW VISIBLE \"U SEE NOTHING\""
    --> Err [{ problem = ExpectingEnd, col = 1, row = 4 }]

Por último, a veces necesitamos decidir si hacemos fallar un parser según su contenido ya parseado. Por ejemplo, si necesitáramos un parser que hiciera fallar los programas que empiezan con dos comandos «HAI» seguidos, podríamos usar

singleVersionProgramP : Parser (List Command)
singleVersionProgramP =
    fullProgramP
        |> Parser.andThen
            (\commands ->
                case commands of
                    (Version _) :: (Version _) :: _ ->
                        Parser.problem "Multiple versions defined"

                    _ ->
                        Parser.succeed commands
            )

Parser.andThen puede inspeccionar el contenido de un parser y devuelve un nuevo parser según su valor. Aquí, hicimos fallar el parser con un mensaje personalizado usando Parser.problem en un caso, y devolvimos el valor original en el otro.

Parser.run singleVersionProgramP "HAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Ok [Version 2, Import "stdio"]
Parser.run singleVersionProgramP "HAI 1\nHAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Err [{ problem = Problem "Multiple versions defined", col = 8, row = 4 }]

Instrucciones

Te emociona unirte a Paola's Prestigious Pizza como el nuevo gerente del restaurante. Tienes muchos planes para dirigir el lugar de forma eficiente y, para lograrlo, necesitas algunos datos. Lo primero es lo primero: necesitas analizar el menú.

Hay muchos tipos de pizza en el menú; Paola te dio una lista en un archivo de texto, con una pizza por línea. Cada entrada de pizza tiene tres componentes: su nombre, un indicador vegetariano opcional y un precio.

1. Analiza el precio de la pizza

Miras las primeras líneas del menú:

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

El precio parece un buen punto de partida. Todos los precios son números enteros en euros.

Implementa priceParser para analizar los precios. Como todos los precios están en euros, no necesitas llevar un registro de la moneda, aunque sí necesitas analizarla.

Parser.run priceParser "8€"
    --> Ok 8
Parser.run priceParser "8"
    --> Err [{ problem = ExpectingSymbol "€", col = 2, row = 1 }]

2. Analiza el indicador vegetariano

Miras el menú otra vez:

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

El indicador vegetariano "(v)" también es bastante sencillo, ¡así que decides hacer eso a continuación!

Implementa vegetarianParser para analizar el indicador. El parser debe devolver un Bool, True si el indicador está presente y False en caso contrario.

Parser.run vegetarianParser "(v)"
    --> Ok True
Parser.run vegetarianParser ""
    --> Ok False

3. Analiza los nombres de las pizzas y los ingredientes

Muy bien, ¿qué sigue?

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Los ingredientes y los nombres de las pizzas parecen bastante sencillos: todos son palabras individuales formadas por caracteres ASCII en mayúsculas y minúsculas.

Implementa wordParser para analizar los nombres. Ya que estás, convierte todas las palabras a minúsculas para que sean uniformes.

Parser.run wordParser "REGINA"
    --> Ok "regina"
Parser.run wordParser "tomato"
    --> Ok "tomato"
Parser.run wordParser "(v)"
    --> Ok ""
Parser.run wordParser ""
    --> Ok ""

4. Analiza una lista de ingredientes

Sin embargo, los ingredientes vienen en grupo:

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Son palabras individuales separadas por comas y, posiblemente, espacios.

Implementa ingredientsParser para analizar los ingredientes.

Parser.run ingredientsParser "tomato, emmental"
    --> Ok ["tomato", "emmental"]
Parser.run ingredientsParser "tomato"
    --> Ok ["tomato"]

5. Analiza la pizza completa

Ahora que tienes todos los ingredientes, ¡es hora de preparar una Pizza!

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Primero tienes el nombre de la pizza, el indicador vegetariano opcional, dos puntos ':', la lista de ingredientes, un guion '-' y el precio, con posibles espacios intercalados.

Implementa pizzaParser para analizar la pizza completa.

Parser.run pizzaParser "Regina: tomato, ham, mushrooms, cantal - 11€"
    --> Ok (Pizza "regina" False ["tomato", "ham", "mushrooms", "cantal"] 11)

6. Analiza el menú completo

¡Hora de festejar!

Implementa menuParser para analizar una lista de todas las pizzas separadas por caracteres de salto de línea '\n' en el archivo de texto. Asegúrate de obtener todas las pizzas ejecutando el parser hasta que llegue al final del archivo.

Parser.run menuParser "Regina: tomato, ham, mushrooms, cantal - 11€\nFormaggio (v): tomato, emmental - 8€"
    --> Ok [Pizza "regina" False ["tomato", "ham", "mushrooms", "cantal"] 11,
    --      Pizza "formaggio" True ["tomato", "emmental"] 8]
Parser.run menuParser "Regina: tomato, ham, mushrooms, cantal - 11€\[END]"
    --> Err [{ problem = ExpectingEnd, col = 1, row = 2 }]

7. Analiza nombres de ingredientes de varias palabras

Oh no, parece que se te pasó algo. En alguna parte más abajo en el menú, algunos ingredientes no son palabras individuales:

...
Tonno: tomato sauce, tuna - 10€
Hawaii: tomato sauce, fresh pineapple, ham - 9€
...

Implementa oneIngredientParser, que aceptará caracteres ASCII en mayúsculas y minúsculas o espacios ' '. Aprovecha también para corregir un pequeño defecto que tenía wordParser: asegúrate de que los strings vacíos no se reconozcan como ingredientes válidos y, en su lugar, emitan Problem "empty string". Ya que estás, asegúrate de convertir los strings a minúsculas y también de eliminar los espacios en blanco de ambos lados.

Parser.run oneIngredientParser "Tomato Sauce"
    --> Ok "tomato sauce"
Parser.run oneIngredientParser "   tomato sauce     "
    --> Ok "tomato sauce"
Parser.run oneIngredientParser ""
    --> Err [{ problem = Problem "empty string", col = 1, row = 1 }]

Una vez que el parser esté definido, úsalo en ingredientsParser.

Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
Elm Exercism

¿Todo listo para empezar La prestigiosa pizza de Paola?

Regístrate en Exercism para aprender y dominar Elm con 28 conceptos110 ejercicios y mentoría humana real, todo gratis.