Как "отлаживать" Haskell с помощью printfs?

входящий в сообщество Ocaml, я пытаюсь изучить немного Haskell. Переход идет неплохо, но я немного путаюсь с отладкой. Я использовал (много) "printf" в своем коде ocaml, чтобы проверить некоторые промежуточные значения или как флаг, чтобы увидеть, где вычисление точно потерпело неудачу.

Так как printf является действием IO, мне нужно поднять весь мой код haskell внутри монады IO, чтобы иметь возможность такого отладки? Или есть лучший способ сделать это (я действительно не хочу делать это вручную, если его можно избежать)

Я также нашел функцию trace: http://www.haskell.org/haskellwiki/Debugging#Printf_and_friends который кажется именно тем, что я хочу, но я не понимаю его типа: нет IO в любом месте! Может ли кто-нибудь объяснить мне поведение функции трассировки?

Ответ 1

trace - самый простой в использовании метод для отладки. Это не в IO именно по той причине, по которой вы указали: нет необходимости поднять ваш код в монаде IO. Это реализовано как

trace :: String -> a -> a
trace string expr = unsafePerformIO $ do
    putTraceMsg string
    return expr

Таким образом, за сценой находится IO, но unsafePerformIO используется для выхода из него. Это функция, которая потенциально нарушает ссылочную прозрачность, которую вы можете угадать, глядя на ее тип IO a -> a, а также его имя.

Ответ 2

trace просто делается нечистым. Точка монады IO состоит в том, чтобы сохранить чистоту (без IO незаметно для системы типов) и определить порядок выполнения инструкций, которые в противном случае были бы практически undefined через ленивую оценку.

Однако на свой собственный риск вы можете взломать часть IO a -> a, т.е. выполнить нечистый ввод-вывод. Это взлом и, конечно, "страдает" от ленивой оценки, но то, что трассировка просто делает для отладки.

Тем не менее, вы, вероятно, должны пойти другими способами для отладки:

1) Уменьшение необходимости отладки промежуточных значений

  • Напишите небольшие, многоразовые, ясные, общие функции, чья правильность очевидна.
  • Объедините правильные части с более правильными деталями.
  • Напишите тесты или попробуйте выполнить фрагменты в интерактивном режиме

2)

  • Использовать точки останова и т.д. (отладка на основе компилятора)

3)

  • Используйте общие монады. Если ваш код монодичен, напишите его независимо от конкретной монады. Используйте type M a = ... вместо обычного IO .... После этого вы можете легко комбинировать монады с помощью трансформаторов и накладывать на нее отладочную монаду. Даже если необходимость в монадах исчезнет, ​​вы можете просто вставить Identity a для чистых значений.

Ответ 3

Для чего это стоит, на самом деле существуют два типа "отладки":

  • Ведение промежуточных значений журнала, таких как значение, которое определенное подвыражение имеет для каждого вызова в рекурсивной функции
  • Проверка поведения среды выполнения выражения

В строгом императивном языке они обычно совпадают. В Haskell они часто не делают:

  • Запись промежуточных значений может изменить поведение во время выполнения, например, заставляя оценивать термины, которые иначе были бы отброшены.
  • Фактический процесс вычисления может существенно отличаться от кажущейся структуры выражения из-за лени и общих подвыражений.

Если вы просто хотите сохранить журнал промежуточных значений, есть много способов сделать это - например, вместо того, чтобы поднимать все на IO, достаточно простой монады Writer, что эквивалентно созданию функции возвращают 2-кортеж их фактического результата и значение аккумулятора (какой-то список, как правило).

Также обычно не нужно помещать все в монаду, только функции, которые нужно записать в значение "log" - например, вы можете разделить только подвыражения, которые могут потребоваться для ведения журнала, оставив основной логически чистой, а затем собрать общий расчет, объединив чистые функции и каротажные вычисления обычным образом с помощью fmap и еще чего. Имейте в виду, что Writer - это своего рода извиняемое оправдание монаде: никоим образом не читать из журнала, только писать ему, каждое вычисление логически не зависит от его контекста, что упрощает жонглирование вокруг.

Но в некоторых случаях даже этот излишний - для многих чистых функций просто перемещение подвыражений в верхний уровень и попытки использования в REPL работает очень хорошо.

Если вы хотите фактически проверить поведение во время выполнения чистого кода, однако, например, чтобы выяснить, почему расщепление подвыражения расходятся - в общем, нет способа сделать это из другого чистого кода - на самом деле, это по существу определение чистоты. Таким образом, в этом случае у вас нет выбора, кроме как использовать инструменты, которые существуют "за пределами" чистого языка: либо нечистые функции, такие как unsafePerformPrintfDebugging - errr, я имею в виду trace - или измененная среда выполнения, например Отладчик GHCi.

Ответ 4

trace также имеет тенденцию переоценивать свой аргумент для печати, теряя при этом много преимуществ лени в этом процессе.

Ответ 5

Если вы можете подождать, пока программа не будет закончена, прежде чем приступать к изучению вывода, то класть Writer monad - это классический подход к внедрению регистратора. Я использую этот здесь, чтобы вернуть набор результатов из нечистого кода HDBC.

Ответ 6

Хорошо, так как целые Haskell построены вокруг принципа ленивой оценки (так что порядок вычислений фактически не детерминирован), использование printf в нем мало имеет смысла.

Если REPL + проверять результирующие значения на самом деле недостаточно для вашей отладки, то все, что связано с IO, является единственным выбором (но это НЕ ПРАВИЛЬНЫЙ ПУТЬ программирования Haskell).