Как правильно выполнить рекурсивный анализ?

У меня есть такой код:

-- my parser, takes a function that takes a string and gives 
-- the suffix and the answer
data Parser a = MkParser (String -> Maybe (String, a))

unParser :: Parser a -> String -> Maybe (String, a)
unParser (MkParser sf1) = sf1

-- gives `a` as the answer, never fails, doesn't change input string
pure :: a -> Parser a
pure a = MkParser (\inp -> Just (inp, a))

-- Choice... if pa fails, try pa2
(<|>) :: Parser a -> Parser a -> Parser a
MkParser sf1 <|> p2 = MkParser g
  where
    g inp = case sf1 inp of
              Nothing -> unParser p2 inp
              j -> j        -- the Just case

-- | 0 or more times, collect the answers into a list.
many :: Parser a -> Parser [a]
many p = some p <|> pure []
-- Explanation: To repeat 0 or more times, first try 1 or more
-- times!  If that fails, then we know it's 0 times, and the answer is the
-- empty list.

-- | 1 or more times, collect the answers into a list.
some :: Parser a -> Parser [a]
some p = liftA2 (:) p (many p)
-- Explanation: To repeat 1 or more times, do 1 time, then 0 or more times!

liftA2 :: (a -> b -> c) -> Parser a -> Parser b -> Parser c
liftA2 op (MkParser sf1) p2 = MkParser g
  where
    g inp = case sf1 inp of
              Nothing -> Nothing
              Just (middle, a) ->
                  case unParser p2 middle of
                    Nothing -> Nothing
                    Just (rest, b) -> Just (rest, op a b)

и я тестирую код с помощью этого базового парсера, который просто дает успех, если это то char, которое я ищу:

char :: Char -> Parser Char
char wanted = MkParser sf
  where
    sf (c:cs) | c == wanted = Just (cs, c)
    sf _ = Nothing

Для запуска парсеров я использую это

runParser :: Parser a -> String -> Maybe a
runParser (MkParser sf) inp = case sf inp of
                                Nothing -> Nothing
                                Just (_, a) -> Just a

Теперь я делаю runParser (many (char '!')) "testing" и получаю Just "".

Я пытаюсь пройти через код. Сначала (many (char '!')) звонит some (char '!').

Это вызывает liftA2 (:) p (many p). И вот many p звонит liftA2 (:) p (many p) снова. Это повторяется бесконечно, верно? Я новичок в Haskell, но предполагаю, что из-за ленивых вычислений он в конечном итоге пытается вычислить p?. runParser (char '!') "testing" дает Nothing, следовательно, первый liftA2 (:) p (many p) тоже дает Nothing, верно? Это переходит ко второму случаю some p <|> pure [] и pure [] дает Just []. Поэтому ответ должен быть Just [].

Но ответ, который я получаю, когда бегу runParser (many (char '!')) "Testing", — Just "". Откуда взялся ""?

Связанный: Правильны ли эти шаги?

many (char '!') = some (char '!') <|> pure []
-- calls some p
some (char '!') = liftA2 (:) (char '!') (many (char '!'))
-- here, it ways for the third argument of liftA2, or does it
-- start trying to run liftA2 with the second argument?
-- (many (char '!')) calls this:
many (char '!') = some (char '!') <|> pure []
-- which then again calls 
some (char '!') = liftA2 (:) (char '!') (many (char '!'))
-- so now it looks like this:
liftA2 (:) (char '!') ( liftA2 (:) (char '!') (many (char '!')) )
-- When does it start evaluating? 
-- Because `(many (char '!')` can't technically run because
-- it needs to wait for the third argument of 
liftA2 (:) (char '!') (many (char '!'))
-- which never gives an answer since it keeps recursing right?

Если бы мне пришлось гадать:

many (char '!') = some (char '!') <|> pure []
some (char '!') = liftA2 (:) (char '!') (many (char '!'))
-- run `liftA2 (:) (char '!')`, get `Nothing`
-- go to the second choice of <|>
<|> pure []
-- give Just []
Just []

Это правильно?

Еще 1 изменение: (Извините, мне следовало добавить эти вопросы в исходное сообщение, но, поскольку все они связаны, я не хочу создавать еще один вопрос)

Если я сделаю

runParser (many (char '!')) "!esting"

Шаги:

(many (char '!'))
many p = some p <|> pure []
-- calls `some p`
some (char '!') = liftA2 (:) (char '!') (many (char '!'))
-- the first `char !` (second argument) runs, and it is a success.
-- the success gives Just ("esting", '!') to the third
-- argument of liftA2.
-- Now it goes to the third argument of liftA2, which is
(many (char '!')
-- which calls
many p = some p <|> pure [] -- but the string is "esting"
-- this calls
some (char '!') = liftA2 (:) (char '!') (many (char '!'))
-- the string is "esting"
-- this fails, so it tries the second choice of
some p <|> pure []
-- which is `pure []` which just gives []

-- Now we go back to the first
liftA2 (:) (char '!') (many (char '!'))
-- where the first answer is '!' and the second answer is '[]'
-- and it combines it with `:`. So we have
-- Just ("sting", '!' : '[]')
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Обратите внимание, что "" — это то же самое, что [].

В Haskell пустой список типа [a] печатается как [], за исключением случаев, когда a = Char. Для [Char], также известного как String, он печатается как "".

Непустые списки Char также имеют специальное обозначение: вместо ['H', 'e', 'l', 'l', 'o'] мы получаем "Hello" в качестве вывода. Тем не менее, эти два значения совершенно одинаковы.


Что касается шагов вычислений, порядок вычислений в Haskell сложен, поскольку при вызове f x y z именно f решает, что оценивать среди своих аргументов. Сначала он мог оценить x, а затем, в зависимости от результата, оценить y или z. Или вместо этого он может начать с оценки z...

В любом случае, к счастью, порядок не имеет большого значения. Пока вы расширяете определения, как вы это сделали, и пока вы не застреваете в бесконечном цикле (или другом нижнем значении), ваши шаги семантически верны. В худшем случае вы оцените больше, чем нужно, но конечный результат всегда один и тот же.

many (char '!') = some (char '!') <|> pure []
-- calls some p

Правильно, поскольку <|> решает сначала попробовать первый парсер.

some (char '!') = liftA2 (:) (char '!') (many (char '!'))
-- here, it ways for the third argument of liftA2, or does it
-- start trying to run liftA2 with the second argument?

Грубо говоря, liftA2 начинается со второго аргумента. В зависимости от этого третий может быть оценен позже.

-- (many (char '!')) calls this:
many (char '!') = some (char '!') <|> pure []
-- which then again calls 
some (char '!') = liftA2 (:) (char '!') (many (char '!'))
-- so now it looks like this:
liftA2 (:) (char '!') ( liftA2 (:) (char '!') (many (char '!')) )
-- When does it start evaluating?

Это правильно, но мы никогда до этого не доходим. char '!' в вашей входной строке завершается с ошибкой Nothing, и, видя это, liftA2 останавливает вычисление с результатом Nothing.

Теперь мы попробуем вторую сторону .. <|> pure [], отсюда и финальную Just [], или, скорее, Just "" в том виде, в каком она на самом деле напечатана.

(Обратите внимание, что это выполнение происходит с вашей входной строкой «Тестирование». Конечно, в других строках мы можем получить другое поведение.)

Спасибо. Я только что отредактировал свой пост, чтобы попытаться прояснить фактические шаги. Я не понимаю, в какой момент Haskell начинает оценивать liftA2. Потому что liftA2 работает только со вторым парсером, но второй парсер никогда не передается как есть (many (char '!')), который продолжает вызывать liftA2 снова? Правильно ли мое решение?

user20102550 23.07.2024 22:28

@user20102550 user20102550 Добавил более подробную информацию об этом.

chi 23.07.2024 22:32

Спасибо. Да, я только что понял, что лучше попробовать другую входную строку, которая действительно дает успех, чтобы посмотреть, пойму ли я шаги лучше. Я отредактировал свое сообщение и попытался просмотреть код, если входная строка «!esting». Если хотите, можете еще раз проверить, правильно ли мое решение! Я приму ваш ответ несмотря ни на что, поскольку он отвечает на исходный вопрос. Спасибо!

user20102550 23.07.2024 22:47

@user20102550 user20102550 Это выглядит на 99% правильно. Если говорить более педантично, третий аргумент LiftA2 не получает Just ("esting", '!'), а только часть "esting". Это связано с тем, чтоliftA2 передает только неиспользованную строку. '!' вместо этого используется позже для формирования окончательного результата, как вы это сделали. Однако окончательный результат Just ("esting", '!' : []), а не Just ("sting", '!' : '[]').

chi 23.07.2024 23:41

Другие вопросы по теме