Percursos
/
Elm
Elm
/
Exercícios
/
A prestigiada pizza da Paola
A prestigiada pizza da Paola

A prestigiada pizza da Paola

Exercício de aprendizagem

Introdução

Parsing

O parsing é o processo de transformar texto em dados com significado. As funções do pacote String são limitadas nesse aspeto e as expressões regulares são difíceis de usar, por isso a forma idiomática de fazer parsing em Elm é com o pacote elm/parser.

Vamos ver um exemplo de como escrever um parser para (um subconjunto de) a linguagem de programação LOLCODE. Considera o seguinte programa comentado:

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

Primeiro, vamos escrever um tipo de Elm para representar os comandos possíveis:

type Command
    = Version Int
    | Import String
    | Print String

O nosso programa em LOLCODE pode então ser modelado como uma List Command. Vamos começar com um parser para a linha HAI 1:

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

O versionP é construído com um parser pipeline. O operador (|.) significa "analisa a string mas descarta o resultado" e o (|=) significa "analisa a string e mantém o resultado". Neste exemplo, Parser.keyword "HAI" vai consumir um "HAI", mas esse resultado é ignorado. Parser.spaces vai consumir qualquer número de carateres ' ', '\n' e '\r', mas isso também será ignorado. Parser.int vai analisar um número inteiro, e o valor desse inteiro (digamos 1) será passado ao Parser.succeed Version do topo, e o parser terá sucesso ao devolver o valor Version 1.

Se algum dos parsers do pipeline falhar, devolve um Parser.DeadEnd e o parser todo falha. Um parser pode ser executado com 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 tratar da linha "CAN HAS STDIO?" com

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

O importP também é um parser pipeline, mas desta vez termina com Parser.symbol, que é adequado para analisar símbolos como "?". O valor que queremos manter (o nome do pacote a importar) é analisado com

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

Como não sabemos qual será o nome do pacote, não podemos usar Parser.keyword; em vez disso, usamos Parser.chompWhile, que vai consumir carateres individuais desde que satisfaçam uma condição (aqui, serem alfanuméricos). Esses carateres consumidos são depois recolhidos por Parser.getChompedString, que devolve um Parser String. Neste caso, queremos normalizar os nomes dos pacotes, por isso usamos Parser.map para modificar a string contida no parser e pô-la em 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 }]

A seguir, VISIBLE "HAI WORLD!" é analisado com

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!")

Agora temos um parser para cada comando. Claro que, num programa geral, a ordem destes comandos não pode ser prevista, por isso construímos um parser de comandos genérico

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

Parser.oneOf vai tentar um parser de cada vez, pela ordem dada. Se um parser falhar, tenta-se o seguinte, mas se tiver sucesso, o seu resultado é devolvido sem tentar os restantes parsers. Se todos os parsers falharem, Parser.oneOf falha.

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 }
    --      ]

Repara como cada tentativa falhada produz um Parser.DeadEnd, que é recolhido por Parser.oneOf.

Para analisar um programa completo, temos de analisar vários comandos sequencialmente com

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

Parser.sequence é um parser muito personalizável, com várias opções. start, separator e end recebem strings que separam os itens da sequência; para analisar uma lista de Elm, usaríamos "[", "," e "]". spaces dá flexibilidade ao receber um parser que trata dos possíveis espaços entre itens e separadores; para analisar uma lista de Elm usaríamos Parser.spaces, mas neste caso não aceitamos espaços, por isso usamos Parser.succeed (), que tem sucesso imediatamente sem consumir nada. item recebe o parser dos itens e, por fim, trailing permite-nos decidir se os separadores finais são Parser.Forbidden, Parser.Mandatory ou 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 }
    --      ]

Quando os parsers têm sucesso, param de consumir a string

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

Por isso, se precisarmos de garantir que analisámos tudo na 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 fim, às vezes precisamos de decidir falhar um parser consoante o seu conteúdo analisado. Por exemplo, se precisássemos de um parser que falhasse programas que começam com dois comandos "HAI" seguidos, podí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 pode inspecionar o conteúdo de um parser e devolve um novo parser consoante o seu valor. Aqui, falhámos o parser com uma mensagem personalizada usando Parser.problem num dos casos e devolvemos o valor original no outro.

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 }]

Instruções

Estás entusiasmado por te juntares à Paola's Prestigious Pizza como novo gerente do restaurante. Tens muitos planos para gerir o restaurante de forma eficiente e, para isso, precisas de alguns dados. Antes de mais, precisas de analisar o menu.

Há muitos tipos de pizza no menu. A Paola deu-te uma lista num ficheiro de texto, com uma pizza por linha. Cada entrada de pizza tem três componentes: o nome, um indicador de vegetariano opcional e um preço.

1. Interpreta o preço da pizza

Olhas para as primeiras linhas do menu:

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

O preço parece um bom ponto de partida. Todos os preços são números inteiros em euros.

Implementa o priceParser para interpretar os preços. Como todos os preços são em euros, não precisas de acompanhar a moeda, mas mesmo assim tens de a interpretar.

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

2. Interpreta o indicador de vegetariano

Olhas outra vez para o menu:

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

O indicador de vegetariano "(v)" também é bastante simples, por isso decides fazê-lo a seguir!

Implementa o vegetarianParser para interpretar o indicador. O parser deve devolver um Bool, True se o indicador estiver presente e False caso contrário.

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

3. Interpreta os nomes das pizzas e dos ingredientes

Muito bem, o que vem a seguir?

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

Os nomes dos ingredientes e das pizzas parecem bastante simples: são todos palavras únicas formadas por carateres ASCII maiúsculos e minúsculos.

Implementa o wordParser para interpretar os nomes. Já agora, passa todas as palavras para minúsculas, para uniformizar.

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

4. Interpreta uma lista de ingredientes

No entanto, os ingredientes vêm em grupo:

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

São palavras únicas separadas por vírgulas e, possivelmente, espaços.

Implementa o ingredientsParser para interpretar os ingredientes.

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

5. Interpreta uma pizza completa

Agora que tens todos os ingredientes, está na hora de fazer uma Pizza!

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

Primeiro tens o nome da pizza, o indicador de vegetariano opcional, dois pontos ':', a lista de ingredientes, um hífen '-' e o preço, com possíveis espaços pelo meio.

Implementa o pizzaParser para interpretar a pizza completa.

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

6. Interpreta o menu completo

Hora de festa!

Implementa o menuParser para interpretar uma lista de todas as pizzas separadas por carateres de nova linha '\n' no ficheiro de texto. Certifica-te de que apanhaste todas as pizzas, executando o parser até chegar ao fim do ficheiro.

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. Interpreta nomes de ingredientes com várias palavras

Oh, não, parece que te escapou uma coisa. Mais abaixo no menu, alguns ingredientes não são palavras únicas:

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

Implementa o oneIngredientParser, que vai aceitar carateres ASCII maiúsculos e minúsculos ou espaços ' '. Aproveita também para corrigir uma pequena falha que o wordParser tinha, garantindo que as strings vazias não são reconhecidas como ingredientes válidos e que, em vez disso, é emitido Problem "empty string". Já agora, garante que passas as strings para minúsculas e que aparam os carateres de espaço em branco de ambos os 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 }]

Depois de definires o parser, usa-o no ingredientsParser.

Editar via GitHub A ligação abre numa nova janela ou separador
Elm Exercism

Estás pronto para começar A prestigiada pizza da Paola?

Inscreve-te no Exercism para aprenderes e dominares Elm com 28 conceitos110 exercícios, e mentoria humana real, tudo grátis.