Как "отладить" Haskell с помощью printfs?
Исходя из сообщества Ocaml, я пытаюсь немного изучить Haskell. Переход проходит довольно хорошо, но я немного запутался с отладкой. Я использовал (много) "printf" в моем коде ocaml, чтобы проверить некоторые промежуточные значения, или как флаг, чтобы увидеть, где вычисление точно провалилось.
Так как printf - это IO- действие, нужно ли мне поднимать весь мой код haskell внутри IO- монады, чтобы иметь возможность отладки такого рода? Или есть лучший способ сделать это (я действительно не хочу делать это вручную, если этого можно избежать)
Я также нахожу функцию трассировки: http://www.haskell.org/haskellwiki/Debugging, которая, кажется, именно то, что я хочу, но я не понимаю, ее тип: нигде нет ввода-вывода! Может кто-нибудь объяснить мне поведение функции трассировки?
6 ответов
trace
это самый простой в использовании метод для отладки. Это не в IO
именно по той причине, которую вы указали: нет необходимости поднимать код в IO
монада. Это реализовано так
trace :: String -> a -> a
trace string expr = unsafePerformIO $ do
putTraceMsg string
return expr
Так что IO негласно, но unsafePerformIO
используется, чтобы вырваться из него. Это функция, которая потенциально нарушает ссылочную прозрачность, которую вы можете догадаться, глядя на ее тип IO a -> a
а также его имя.
trace
просто нечистым Суть IO
Монадой является сохранение чистоты (без ввода-вывода, незаметного для системы типов) и определение порядка выполнения операторов, который в противном случае был бы практически неопределенным из-за ленивых вычислений.
Однако на свой страх и риск вы можете взломать IO a -> a
, т.е. выполнить нечистый IO. Это хак и, конечно, "страдает" от ленивых вычислений, но это то, что трассировка просто делает для отладки.
Тем не менее, вы, вероятно, должны пойти другими путями для отладки:
Уменьшение необходимости отладки промежуточных значений
- Напишите небольшие, многократно используемые, понятные, универсальные функции, правильность которых очевидна.
- Объедините правильные части в большие правильные части.
- Пишите тесты или тестируйте части в интерактивном режиме.
Используйте точки останова и т. Д. (Отладка на основе компилятора)
Используйте общие монады. Если ваш код тем не менее является монадическим, напишите его независимо от конкретной монады. использование
type M a = ...
вместо простогоIO ...
, После этого вы можете легко комбинировать монады через трансформаторы и помещать монаду отладки поверх нее. Даже если потребность в монаде исчезла, вы можете просто вставитьIdentity a
для чистых ценностей.
Для чего бы это ни стоило, на самом деле здесь есть два вида "отладки":
- Регистрация промежуточных значений, таких как значение, которое конкретное подвыражение имеет при каждом вызове, в рекурсивную функцию
- Проверка поведения во время выполнения оценки выражения
На строгом императивном языке они обычно совпадают. В Хаскеле они часто не делают:
- Запись промежуточных значений может изменить поведение среды выполнения, например, путем принудительного вычисления терминов, которые в противном случае были бы отброшены.
- Фактический процесс вычислений может резко отличаться от кажущейся структуры выражения из-за лени и общих подвыражений.
Если вы просто хотите вести журнал промежуточных значений, есть много способов сделать это - например, вместо того, чтобы поднимать все в IO
, просто Writer
Достаточно монады, что эквивалентно тому, что функции возвращают 2 кортежа их фактического результата и значение аккумулятора (обычно своего рода список).
Также обычно нет необходимости помещать все в монаду, только функции, которые нужно записать в значение "log" - например, вы можете выделить только те подвыражения, которые могут понадобиться для ведения журнала, оставляя основную логику чистой, затем пересобрать все вычисления, комбинируя чистые функции и записывая вычисления обычным образом с fmap
с и еще много чего. Имейте в виду, что Writer
это своего рода простое оправдание для монады: без возможности чтения из журнала, только записи в него, каждое вычисление логически не зависит от контекста, что облегчает манипулирование вещами.
Но в некоторых случаях даже это излишне - для многих чистых функций достаточно просто переместить подвыражения на верхний уровень и опробовать вещи в REPL.
Однако если вы хотите на самом деле проверить поведение чистого кода во время выполнения - например, чтобы выяснить, почему подвыражение расходится, - в общем случае нет никакого способа сделать это из другого чистого кода- на самом деле это по существу определение чистоты. Таким образом, в этом случае у вас нет выбора, кроме как использовать инструменты, которые существуют "вне" чистого языка: либо нечистые функции, такие как unsafePerformPrintfDebugging
я имею в виду trace
- или измененную среду выполнения, такую как отладчик GHCi.
trace
также имеет тенденцию переоценивать свои аргументы в пользу печати, теряя многие преимущества лени в процессе.
Если вы можете подождать, пока программа не закончит работу, прежде чем изучать вывод, то укладка монады Writer является классическим подходом к реализации регистратора. Я использую это здесь, чтобы вернуть набор результатов из нечистого кода HDBC.
Ну, поскольку весь Haskell построен на принципе ленивой оценки (так что порядок вычислений фактически недетерминирован), использование printf в этом имеет мало смысла.
Если REPL+ проверить результирующие значения действительно недостаточно для вашей отладки, оборачивать все в IO - единственный выбор (но это не ПРАВИЛЬНЫЙ способ программирования на Haskell).