Как использовать ответы парсеров при вложении парсеров (последовательно)

Это мой парсер

data Parser a = MkParser (String -> Maybe (String, a))

Это анализатор, который анализирует, выполняется ли конкретный предикат.

satisfy :: (Char -> Bool) -> Parser Char
-- takes a predicate (takes a item and gives True or False)
-- Nothing if it's False, parse if it's true
satisfy pred = MkParser sf
    where
        sf (c:cs) | pred c = Just (cs, c)
        sf _ = Nothing

Этот парсер просто дает то, что я хочу, и не меняет входную строку:

pure :: a -> Parser a
pure a = MkParser sf
    where
        sf inp = Just (inp, a)

Теперь у меня есть этот парсер. Требуется синтаксический анализатор, и ответ передается функции, которая анализирует полученный ответ.

dsc :: Parser a -> (a -> Parser b) -> Parser b
dsc (MkParser pa) pb = MkParser sf
    where
        sf inp = case pa inp of
            Nothing -> Nothing
            Just (cs, c) -> unParser (pb c) cs


-- this just runs a parser
unParser :: Parser a -> String -> Maybe (String, a)
unParser (MkParser pa) inp = pa inp

Теперь я пытаюсь проанализировать строку. Если это буква, то цифра, то я хочу дать букву и цифру:

myldV2 :: Parser String
myldV2 = dsc (dsc (satisfy isAlpha) (\x-> satisfy isDigit)) (\y -> pure [x,y])

Но там написано, что x вне диапазона. Мое решение состояло в том, чтобы воссоздать его вот так, и оно работает (по сути, я сделал это встроенным парсером): Понимание использования скобок в Haskell - Парсер, который зависит от предыдущего парсера, выдает ошибку при использовании скобок

Мое второе решение тоже работает:

lde = dsc p1 f1
    where
        p1 = satisfy isAlpha
        f1 x = dsc p2 f2
            where
                p2 = satisfy isDigit
                f2 y = dsc p3 f3
                    where
                        p3 = char '!'
                        f3 = \_ -> pure [x,y]

Но как мне заставить мое первое решение работать? Этот myldV2 = dsc (dsc (satisfy isAlpha) (\x-> satisfy isDigit)) (\y -> pure [x,y])

По сути, я хочу знать, что я делаю неправильно и как это исправить, чтобы лучше понимать Haskell и что происходит. Спасибо.

🤔 А знаете ли вы, что...
Haskell имеет множество расширений языка, которые позволяют разработчикам настраивать его поведение.


58
2

Ответы:

Используйте этот шаблон кода, где для дополнительного удобства применяется инфикс dsc:

p :: Parser SomeType
p =
  parser1 `dsc` \ x1 ->
  parser2 `dsc` \ x2 ->
  ...
  parserN `dsc` \ xN ->
  pure (someFunction x1 ... xN)

С явными круглыми скобками это звучит так:

p :: Parser SomeType
p =
  parser1 `dsc` (\ x1 ->
  parser2 `dsc` (\ x2 ->
  ...
  parserN `dsc` (\ xN ->
  pure (someFunction x1 ... xN)
  )...))

Обратите внимание, как первый \ x1 -> ... приводит к тому, что область действия x1 достигает самого конца выражения. Другие лямбды аналогичным образом завершают область действия связанной переменной на последней строке кода.

Если хотите, вы можете переписать вышеприведенное, используя dsc в префиксной позиции. Тем не менее, для монадического оператора, такого как dsc, как-то идиоматично использовать action `dsc` \ result -> ...., когда нотацию do использовать нельзя.


Решено

Просто вставьте что-то во второе решение, и вы получите решение, очень похожее на первое. Маленькими шагами вот отправная точка:

lde = dsc p1 f1
    where
        p1 = satisfy isAlpha
        f1 x = dsc p2 f2
            where
                p2 = satisfy isDigit
                f2 y = dsc p3 f3
                    where
                        p3 = char '!'
                        f3 = \_ -> pure [x,y]

Встроенные p3 и f3:

lde = dsc p1 f1
    where
        p1 = satisfy isAlpha
        f1 x = dsc p2 f2
            where
                p2 = satisfy isDigit
                f2 y = dsc (char '!') (\_ -> pure [x,y])

Встроенные p2 и f2:

lde = dsc p1 f1
    where
        p1 = satisfy isAlpha
        f1 x = dsc (satisfy isDigit) (\y -> dsc (char '!') (\_ -> pure [x,y]))

Встроенные p1 и f1:

lde = dsc (satisfy isAlpha) (\x -> dsc (satisfy isDigit) (\y -> dsc (char '!') (\_ -> pure [x,y])))

Если хотите, вы можете сделать часть повторяющейся структуры более заметной с помощью художественных пробелов. Я также поменяю скобки на $, просто чтобы избежать закрывающих скобок в конце, которые портят расширяемость.

lde = dsc (satisfy isAlpha) $ \x ->
      dsc (satisfy isDigit) $ \y ->
      dsc (char '!') $ \_ ->
      pure [x,y]

Надеюсь, вам сразу бросается в глаза связь со стандартной версией do-нотации! Ваш dsc очень похож на (>>=), и если бы вы действительно назвали его, вы могли бы написать, что это будет означать то же самое:

lde = do
    x <- satisfy isAlpha
    y <- satisfy isDigit
    char '!' -- OR, if you want to be explicit about it: _ <- char '!'
    pure [x,y]