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 }]
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.
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 }]
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
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 ""
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"]
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)
¡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 }]
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.
Regístrate en Exercism para aprender y dominar Elm con 28 conceptos110 ejercicios y mentoría humana real, todo gratis.