Непонятный вывод `traceM` при рекурсивном вызове монады функции

Я новичок в Haskell и изучаю, как использовать Haskell для написания компилятора. Я переписал этот код в «Комбинаторах монадических парсеров», используя синтаксис монады, как показано ниже. Казалось, все работает хорошо, но при попытке отследить выполнение traceM я получил сбивчивый результат word. Почему слово «leave do» появилось три раза, а «enter do» — только один раз? (версия ghc — 9.8.2, и результаты ghc и ghci одинаковы)

код:

import Debug.Trace
newtype Parser a = Parser {parse :: String -> [(a, String)]}

zero :: Parser a
zero = Parser $ \_ -> []

item :: Parser Char
item = Parser $ \input -> case input of
                                [] -> []
                                x : xs -> [(x, xs)]

instance Functor Parser where
  fmap f parser = Parser $ \input -> [(f result, input') | (result, input') <- parse parser input]

instance Applicative Parser where
  pure x = Parser $ \input -> [(x, input)]
  p1 <*> p2 = Parser $ \input -> [(mapper result, input'') | (mapper, input') <- parse p1 input, (result, input'') <- parse p2 input']

instance Monad Parser where
  return = pure
  parser >>= f = Parser $ \input -> concat [parse (f result) input' | (result, input') <- parse parser input]

sat :: (Char -> Bool) -> Parser Char
sat predi = item >>= \x -> if predi x then return x else zero

infixl 0 |:
(|:) :: Parser a -> Parser a -> Parser a
p1 |: p2 = Parser $ \input -> parse p1 input ++ parse p2 input 

lower :: Parser Char
lower = sat $ \x -> x >= 'a' && x <= 'z'

upper :: Parser Char
upper = sat $ \x -> x >= 'A' && x <= 'Z'

letter :: Parser Char
letter = lower |: upper

word :: Parser String
word = return "" |: do
  traceM "enter do"
  x <- letter
  traceM $ "x = " ++ show x
  xs <- word
  traceM $ "xs = " ++ show xs
  traceM "leave do" 
  return (x:xs)
main = putStrLn $ show $ parse word "ab"

вывод traceM:

enter do
x = 'a'
xs = ""
leave do
x = 'b'
xs = ""
leave do
xs = "b"
leave do

мой ожидаемый результат traceM выглядит следующим образом (и я не уверен, что ошибаюсь):

enter do
x = "a"
enter do
x = "b"
xs = ""
leave do
xs = ""
xs = "b"
leave do

обновление: я знаю, что <- не является присваиванием, которое на самом деле является синтаксическим сахаром bind. Если быть более точным, мне интересно, почему «enter do» всегда печатается один раз (независимо от длины входной строки), поскольку функция word определенно вызывается много раз.

обновление: Спасибо за комментарий @Naïm. Я получил «удовлетворительную» информацию о трассировке после вставки =<< pure позади traceM. Вот информация о трассировке:

enter do
x = 'a'
xs = ""
leave do
enter do
x = 'b'
xs = ""
leave do
xs = "b"
leave do
enter do

Все потому, что <- — это не задание. Чтобы более наглядно увидеть, что происходит, посмотрите на этот упрощенный пример.

n. m. could be an AI 26.05.2024 08:51
traceM использует unsafePerformIO, поэтому тот факт, что вы видите только один enter do, вероятно, просто означает, что он оценивается только один раз из-за некоторой оптимизации. Вы можете попробовать traceM =<< pure "enter do" обойти оптимизацию или использовать IO для получения более детерминированных результатов.
Naïm Favier 26.05.2024 12:51

@n.m.couldbeanAI Мой комментарий к ответу Чи, я думаю, применим и к вашему упрощенному примеру: недетерминизм объясняет только часть этого поведения.

Daniel Wagner 26.05.2024 17:37
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
77
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Тип вашего парсера включает функцию, возвращающую [(a, String)], то есть все возможные способы (во множественном числе) анализа входной строки.

Следовательно,

do x <- parserA ; y <- parserB

отдаленно похоже на понимание списка

[ ... | x <- parsesOfA , y <- parsesOfB ]

или, если хотите, своего рода императивный цикл:

for x in parsesOfA:
   for y in parsesOfB:
      ...

Если parserA возвращает несколько способов анализа, то parserB запускается несколько раз. Таким образом, наблюдение одного сообщения «вход» и множества сообщений «выход» является ожидаемым поведением.

Это объясняет часть, но не всю ситуацию. В частности, это объясняет, почему после leave стоят две x = 'b'. Но это не объясняет, почему перед enter нет дополнительного x = 'b'. Единственный парсер между traceM "enter do" и traceM $ "x = " ++ show x — это letter, который не может возвращать несколько результатов.

Daniel Wagner 26.05.2024 17:28
Ответ принят как подходящий

Я собираюсь скопировать раздел из старого ответа, который я написал.

Основное различие, которое вам нужно здесь провести, находится прямо здесь, в этом значительно упрощенном примере:

> g = trace "a" $ \() -> trace "b" ()
> g ()
a
b
()
> g ()
b
()

Существует отдельное понятие кэширования функции и кэширования ее вывода. Последнее просто никогда не делается в GHC (хотя см. обсуждение того, что происходит с вашей оптимизированной версией ниже). Первое может показаться глупым, но на самом деле оно не так уж глупо, как вы думаете; вы можете представить себе написание функции, которая, скажем, id верна, если гипотеза Коллатца верна, и not в противном случае. В такой ситуации имеет смысл проверить гипотезу Коллатца только один раз, а затем навсегда кэшировать, следует ли нам вести себя как id или not.

Я подозреваю, хотя и не уверен, что здесь происходит что-то связанное. Когда вы обессахариваете свой блок do, вы видите, что здесь аналогичная ситуация: некоторые из ваших trace находятся внутри лямбды, а некоторые нет.

do
  traceM "enter do"             traceM "enter do" >> -- outside?
  x <- letter               ==> letter >>= \x ->
  traceM $ "x = " ++ show x     traceM ("x = " ++ show x) -- clearly inside

Однако я думаю, что это еще не вся история. Здесь все еще происходит что-то, чего я не понимаю. Причина, по которой я это говорю, заключается в том, что все ваши реализации методов Monad и Applicative помещают вещи в лямбды таким образом, что я ожидаю, что traceM будет выполняться несколько раз. Я подозревал, что оптимизатор выдернул бит traceM ..., но -ddump-simpl со мной не согласен; мы видим, что traceM "enter do" >> letter вынесено как отдельное подвыражение, но мое чтение ядра предполагает, что это все равно должно выполняться traceM один раз при каждом вызове word_rhN. Так что я в тупике на этом этапе. Вот фрагмент, из которого была извлечена первая часть вашего блока, важные части выделены курсивом:

p2_r1pL :: Parser String
[GblId]
p2_r1pL
  = $c>>=_r1pf
      @()
      @String
      (traceM
         @Parser
         Main.$fApplicativeParser
         (GHC.CString.unpackCString# "enter do"#))
      (\ _ [Occ=Dead] -> k_r1pK)

С самого начала определения $c>>=_r1pf мы видим, что оценка traceM не должна распределяться между отдельными оценками p2_r1pL. Я упустил некоторые отвлекающие моменты.

$c>>=_r1pf :: forall a b. Parser a -> (a -> Parser b) -> Parser b
[...]
$c>>=_r1pf
  = \ (@a_a1gY)
      (@b_a1gZ)
      (parser_axd :: Parser a_a1gY)
      (f_axe :: a_a1gY -> Parser b_a1gZ) ->
      $ @...
        @...
        @...
        @...
        ((\ (ds_d1o8 :: ...) -> ds_d1o8)
         `cast` ...
        (\ (input_axf :: String) -> ...)

Таким образом, даже применительно к двум аргументам, как в p2_r1pL, это должно иметь самую внешнюю лямбду, а traceM должно появляться только внутри этой лямбды. ¯\_(ツ)_/¯

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