Trilhas
/
Elm
Elm
/
Exercícios
/
A pizza prestigiosa da Paola
A pizza prestigiosa da Paola

A pizza prestigiosa da Paola

Exercício de aprendizagem

Introdução

Análise sintática

Análise sintática é o processo de transformar texto em dados significativos. As funções do pacote String são limitadas nesse aspecto, e as expressões regulares são difíceis de usar, então a forma idiomática de analisar 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. Considere 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 em Elm para representar os comandos possíveis:

type Command
    = Version Int
    | Import String
    | Print String

Nosso programa LOLCODE pode então ser modelado como um 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

versionP é construído usando um pipeline de parser. O operador (|.) significa "analise a string mas descarte o resultado" e (|=) significa "analise a string e mantenha o resultado". Neste exemplo, Parser.keyword "HAI" vai consumir um "HAI", mas esse resultado é ignorado. Parser.spaces vai consumir qualquer quantidade de caracteres ' ', '\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 para o Parser.succeed Version mais externo, e o parser vai conseguir retornar o valor Version 1.

Se qualquer um dos parsers do pipeline falhar, ele vai retornar um Parser.DeadEnd e o parser inteiro vai falhar. 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 atacar a linha "CAN HAS STDIO?" com

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

importP também é um pipeline de parser, 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 por

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; então, em vez disso, usamos Parser.chompWhile, que vai consumir caracteres um a um enquanto eles satisfizerem uma condição (aqui, ser um caractere alfanumérico). Esses caracteres consumidos são então coletados por Parser.getChompedString, que retorna um Parser String. Neste caso, queremos normalizar os nomes dos pacotes, então usamos Parser.map para modificar a string contida no parser e deixá-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 }]

Em seguida, VISIBLE "HAI WORLD!" é analisado por

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, em um programa geral, a ordem desses comandos não pode ser prevista, então construímos um parser genérico de comandos

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

Parser.oneOf vai tentar um parser, na ordem dada. Se um parser falhar, o próximo é tentado; mas, se ele tiver sucesso, seu resultado é retornado sem tentar os parsers restantes. 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 }
    --      ]

Repare como cada tentativa que falha produz um Parser.DeadEnd que é coletado por Parser.oneOf.

Para analisar um programa completo, precisamos analisar vários comandos em sequência 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 bastante personalizável, com uma série de opções. start, separator e end recebem strings que separam os itens da sequência; para analisar uma lista em Elm, usaríamos "[", "," e "]". spaces dá flexibilidade ao receber um parser que vai lidar com possíveis espaços entre itens e separadores; para analisar uma lista em Elm, usaríamos Parser.spaces, mas neste caso não aceitamos nenhum espaço, então usamos Parser.succeed (), que tem sucesso imediatamente sem consumir nada de fato. item recebe o parser do item e, por fim, trailing nos permite decidir se os separadores no final 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, eles param de consumir a string

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

Então, se precisarmos garantir que analisamos 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 decidir falhar um parser dependendo do conteúdo que ele analisou. Por exemplo, se precisássemos de um parser que falhasse programas que começam com dois comandos "HAI" seguidos, poderí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 retorna um novo parser dependendo do seu valor. Aqui, em um caso falhamos o parser com uma mensagem personalizada usando Parser.problem e, no outro, retornamos o valor original.

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

É com muita empolgação que você entra na Paola's Prestigious Pizza como gerente do restaurante. Você tem muitos planos para administrar o lugar com eficiência e, para isso, precisa de alguns dados. Antes de tudo, você precisa analisar o cardápio.

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

1. Analisar o preço da pizza

Você olha para as primeiras linhas do cardápio:

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

O preço parece um bom lugar para começar. Todos os preços são números inteiros na moeda euro.

Implemente priceParser para analisar os preços. Como todos os preços estão em euros, você não precisa se preocupar em guardar a moeda, mas ainda precisa analisá-la.

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

2. Analisar o indicador vegetariano

Você olha para o cardápio de novo:

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

O indicador vegetariano "(v)" também é bem simples, então você decide fazer isso em seguida!

Implemente vegetarianParser para analisar o indicador. O parser deve retornar um Bool: True se o indicador estiver lá e False caso contrário.

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

3. Analisar nomes de pizzas e ingredientes

Certo, e agora?

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

Os ingredientes e os nomes das pizzas parecem bem simples: cada um é uma única palavra formada por caracteres ASCII maiúsculos e minúsculos.

Implemente wordParser para analisar os nomes. E já que você está nisso, deixe todas as palavras em minúsculas para padronizar.

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

4. Analisar uma lista de ingredientes

Os ingredientes, porém, 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.

Implemente ingredientsParser para analisar os ingredientes.

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

5. Analisar uma pizza completa

Agora que você tem todos os ingredientes, é hora de montar uma Pizza!

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

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

Implemente pizzaParser para analisar a pizza completa.

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

6. Analisar o cardápio completo

Hora de festa!

Implemente menuParser para analisar uma lista de todas as pizzas separadas por caracteres de nova linha '\n' no arquivo de texto. Certifique-se de pegar todas as pizzas rodando o parser até ele chegar ao fim do arquivo.

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. Analisar nomes de ingredientes com mais de uma palavra

Ah, não, parece que você deixou passar alguma coisa. Mais para baixo no cardápio, alguns ingredientes não são palavras únicas:

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

Implemente oneIngredientParser, que vai aceitar caracteres ASCII maiúsculos e minúsculos ou espaços ' '. Aproveite também para corrigir uma pequena falha que wordParser tinha, garantindo que strings vazias não sejam reconhecidas como ingredientes válidos e que, em vez disso, seja emitido Problem "empty string". E já que você está nisso, garanta que as strings fiquem em minúsculas e também remova os espaços em branco das duas pontas.

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 definir o parser, use-o em ingredientsParser.

Editar via GitHub O link abre em uma nova janela ou aba
Elm Exercism

Tudo pronto para começar A pizza prestigiosa da Paola?

Crie sua conta no Exercism para aprender e dominar Elm com 28 conceitos110 exercícios e mentoria humana de verdade, tudo de graça.