3.5. Отладчик GHCi
GHCi содержит простой отладчик в императивном стиле, позволяющий приостанавливать выполняющиеся вычисления, чтобы проверять значения переменных. Отладчик интегрирован в GHCi и включён по умолчанию: для включения средств отладки флаги не требуются. Есть одно важное ограничение: точки останова и пошаговое выполнение доступны только в интерпретируемых модулях; скомпилированный код отладчику недоступен .
Отладчик предоставляет следующие возможности:
- Возможность установить точку останова на определение функции или выражение в программе. Когда функция вызывается или выражение вычисляется, GHCi приостанавливает выполнение и возвращается к приглашению командной строки, где перед продолжением выполнения можно проверить значения локальных переменных.
- Можно выполнять программу пошагово: вычислитель будет приостанавливать выполнение приблизительно после каждой редукции, позволяя проверять локальные переменные. Это равносильно установке точки останова в каждой точке программы.
- Выполнение можно вести в режиме трассировки: вычислитель запоминает каждый шаг вычисления по мере его выполнения, но не приостанавливает выполнение до достижения реальной точки останова. После этого можно просмотреть историю шагов вычисления.
- Исключения (например, ошибка сопоставления с образцом и
error) можно считать точками останова, чтобы помочь найти источник исключения в программе.
В настоящее время получить «трассировку стека» нельзя, однако функции трассировки и истории служат полезной заменой и во многих случаях позволяют установить контекст ошибки. Например, можно автоматически остановить выполнение при возникновении исключения, даже если оно возникло внутри скомпилированного кода (см. Отладка исключений).
-
-fbreak-points -
- По умолчанию:
-
включён для GHCi
Этот флаг позволяет отключить вставку точек останова с помощью обратной формы.
3.5.1. Точки останова и проверка переменных
Для примера воспользуемся быстрой сортировкой. Вот её код:
qsort [] = []
qsort (a:as) = qsort left ++ [a] ++ qsort right
where (left,right) = (filter (<=a) as, filter (>a) as)
main = print (qsort [8, 4, 0, 3, 1, 23, 11, 18])
Сначала загрузим модуль в GHCi:
ghci> :l qsort.hs
[1 of 1] Compiling Main ( qsort.hs, interpreted )
Ok, modules loaded: Main.
*ghci>
Теперь установим точку останова в правой части второго уравнения qsort:
*ghci> :break 2
Breakpoint 0 activated at qsort.hs:2:15-46
*ghci>
Команда :break 2 устанавливает точку останова на строке 2 последнего загруженного модуля, в данном случае qsort.hs. В частности, она выбирает крайнее левое полное подвыражение в этой строке, на котором и устанавливает точку останова; в данном случае это выражение (qsort left ++ [a] ++ qsort right).
Теперь запустим программу:
*ghci> main
Stopped at qsort.hs:2:15-46
_result :: [a]
a :: a
left :: [a]
right :: [a]
[qsort.hs:2:15-46] *ghci>
Выполнение остановлено в точке останова. Приглашение командной строки изменилось, указывая на то, что выполнение остановлено в точке останова, и показывая её расположение: [qsort.hs:2:15-46]. Чтобы уточнить расположение, можно воспользоваться командой :list:
[qsort.hs:2:15-46] *ghci> :list
1 qsort [] = []
2 qsort (a:as) = qsort left ++ [a] ++ qsort right
3 where (left,right) = (filter (<=a) as, filter (>a) as)
Команда :list выводит исходный код вокруг текущей точки останова. Если ваше устройство вывода поддерживает такую возможность, GHCi выделит активное подвыражение полужирным шрифтом.
GHCi создал привязки для свободных переменных выражения, на котором установлена точка останова (a, left, right), а также привязку для результата выражения (_result). Эти переменные ничем не отличаются от других переменных, которые можно определить в GHCi: их можно использовать в выражениях, вводимых в приглашении командной строки, запрашивать их типы с помощью :type и так далее. Однако есть одно важное отличие: типы этих переменных могут быть частичными. Например, если попытаться вывести значение left:
[qsort.hs:2:15-46] *ghci> left
<interactive>:1:0:
Ambiguous type variable `a' in the constraint:
`Show a' arising from a use of `print' at <interactive>:1:0-3
Cannot resolve unknown runtime types: a
Use :print or :force to determine these types
Это происходит потому, что qsort — полиморфная функция, а GHCi не хранит информацию о типах во время выполнения и потому не может определить типы во время выполнения для свободных переменных, содержащих переменные типов. Поэтому, когда вы просите вывести left в приглашении командной строки, GHCi не может определить, какой экземпляр Show использовать, и выдаёт указанную выше ошибку типа.
К счастью, в отладчике есть универсальная команда вывода :print, которая проверяет фактическое значение переменной во время выполнения и пытается восстановить её тип. Попробуем применить её к left:
[qsort.hs:2:15-46] *ghci> :set -fprint-evld-with-show
[qsort.hs:2:15-46] *ghci> :print left
left = (_t1::[a])
Это не особенно полезно. Дело в том, что left связано с ещё не вычисленным вычислением (приостановленным вычислением, или thunk), а :print не запускает вычисление. Идея в том, что :print позволяет проверять значения в точке останова без нежелательных побочных эффектов. Она не запускает вычисление, которое могло бы привести к отличающемуся от обычного результату программы, а значит, не вызывает исключений, бесконечных циклов или срабатывания дополнительных точек останова (см. Вложенные точки останова). Вместо вычисления thunk-компонентов команда :print связывает каждый thunk с новой переменной, имя которой начинается с подчёркивания; в данном случае это _t1.
-
-fprint-evld-with-show -
Флаг -fprint-evld-with-show указывает команде :print по возможности повторно использовать доступные экземпляры Show. Это происходит только в том случае, если содержимое проверяемой переменной вычислено полностью.
Если сохранение степени вычисленности переменной не важно, вместо :print можно использовать :force. Команда :force работает точно так же, как :print, но вычисляет все встреченные thunk-компоненты:
[qsort.hs:2:15-46] *ghci> :force left
left = [4,0,3,1]
Теперь, поскольку :force проверила значение left во время выполнения, ей удалось восстановить его тип. Результат восстановления типа можно увидеть так:
[qsort.hs:2:15-46] *ghci> :show bindings
_result :: [Integer]
a :: Integer
left :: [Integer]
right :: [Integer]
_t1 :: [Integer]
Теперь мы знаем не только тип left, но и типы всех остальных переменных с частичными типами также были уточнены. Поэтому, например, можно запросить значение a:
[qsort.hs:2:15-46] *ghci> a
8
Вместо вычисления всего выражения с помощью :force может быть удобнее использовать функцию Haskell seq для вычисления отдельных thunk-компонентов. Например:
[qsort.hs:2:15-46] *ghci> :print right
right = (_t1::[Integer])
[qsort.hs:2:15-46] *ghci> seq _t1 ()
()
[qsort.hs:2:15-46] *ghci> :print right
right = 23 : (_t2::[Integer])
Мы вычислили только thunk _t1, раскрыв голову списка, а его хвост — ещё один thunk, теперь связанный с _t2. Функцию seq здесь не очень удобно использовать, поэтому можно воспользоваться :def, чтобы создать более удобный интерфейс (это задание оставлено читателю!).
Наконец, можно продолжить текущее выполнение:
[qsort.hs:2:15-46] *ghci> :continue
Stopped at qsort.hs:2:15-46
_result :: [a]
a :: a
left :: [a]
right :: [a]
[qsort.hs:2:15-46] *ghci>
Выполнение продолжилось с места, где оно было остановлено ранее, и теперь снова остановилось в точке останова.
3.5.1.1. Установка точек останова
Точки останова можно устанавливать разными способами. Возможно, проще всего указать имя функции верхнего уровня:
:break identifier
Здесь ⟨identifier⟩ — имя любой функции верхнего уровня из интерпретируемого модуля, загруженного в GHCi (можно использовать квалифицированные имена). Точка останова будет установлена в теле функции после того, как ей будут переданы все аргументы. Если у функции несколько образцов, точка останова будет установлена на каждом из них.
С помощью квалифицированных имён можно устанавливать точки останова на все функции (верхнего уровня и вложенные) во всех загруженных и интерпретируемых модулях:
:break [ModQual.]topLevelIdent[.nestedIdent]...[.nestedIdent]
⟨ModQual⟩ — необязательный параметр, представляющий собой фактическое имя модуля или локальный псевдоним квалифицированного оператора импорта.
⟨topLevelIdent⟩ — имя функции верхнего уровня в модуле, указанном параметром ⟨ModQual⟩.
⟨nestedIdent⟩ — необязательный параметр, представляющий собой имя функции, вложенной в выражение let или where внутри указанной ранее функции ⟨nestedIdent⟩ или ⟨topLevelIdent⟩.
Если ⟨ModQual⟩ — имя модуля, ⟨topLevelIdent⟩ может быть любым идентификатором верхнего уровня в этом модуле. Если ⟨ModQual⟩ не указан или является локальным псевдонимом квалифицированного импорта, ⟨topLevelIdent⟩ должен находиться в области видимости.
Точки останова можно устанавливать на сколь угодно глубоко вложенные функции, но нужно указать всю цепочку имён вложенных функций.
Рассмотрим функцию foo в модуле Main:
foo s = 'a' : add s
where add = (++"z")
Точку останова на функции add можно установить одной из следующих команд:
:break Main.foo.add
:break foo.add
Точки останова также можно устанавливать по номеру строки (и при необходимости столбца):
:break line
:break line column
:break module line
:break module line column
Если точка останова устанавливается на определённой строке, GHCi устанавливает её на крайнее левое подвыражение, которое начинается и заканчивается в этой строке. Если два полных подвыражения начинаются в одном столбце, выбирается самое длинное. Если в строке нет полных подвыражений, выбирается крайнее левое выражение, начинающееся в этой строке, а если такого нет — самое правое выражение, частично или полностью охватывающее эту строку.
Если точка останова устанавливается на определённой строке и в определённом столбце, GHCi выбирает наименьшее подвыражение, содержащее указанное место. Примечание: GHC считает ширину символа табуляции равной 1 независимо от его положения; иными словами, учитываются символы, а не столбцы. Так поступают некоторые редакторы, другие — нет. Лучше всего вообще не использовать символы табуляции в исходном коде (см. -Wtabs в разделе Предупреждения и проверка корректности).
Если модуль не указан, используется последний загруженный модуль.
Не все подвыражения могут быть местами установки точек останова. Отдельные переменные обычно не считаются подходящими местами для точек останова (если только переменная не находится в правой части определения функции, лямбда-выражения или альтернативы case). В общем случае точками останова могут быть все редексы, а также тела функций, лямбда-выражений, альтернатив case и связывающих выражений. Обычно в выражении let точка останова не устанавливается, но она всегда устанавливается в его теле, поскольку чаще всего нас интересуют значения переменных, связанных в let.
Остановившись в точке останова, можно показать текущий стек вычислений с помощью команды :where.
3.5.1.2. Управление точками останова
Список определённых на данный момент точек останова можно вывести с помощью команды :show breaks:
*ghci> :show breaks
[0] Main qsort.hs:1:11-12 enabled
[1] Main qsort.hs:2:15-46 enabled
Чтобы отключить одну или несколько определённых точек останова, используйте команду :disable, указав через пробел один или несколько номеров из вывода команды :show breaks:. Чтобы сразу отключить все точки останова, используйте :disable *.
*ghci> :disable 0
*ghci> :show breaks
[0] Main qsort.hs:1:11-12 disabled
[1] Main qsort.hs:2:15-46 enabled
Отключённые точки останова можно снова включить с помощью команды :enable. Параметры команд :disable и :enable совпадают.
Чтобы удалить точку останова, используйте команду :delete, указав номер из вывода команды :show breaks:
*ghci> :delete 0
*ghci> :show breaks
[1] Main qsort.hs:2:15-46 disabled
Чтобы сразу удалить все точки останова, используйте :delete *.
3.5.2. Пошаговое выполнение
Пошаговое выполнение — отличный способ визуализировать выполнение программы, а также полезный инструмент для поиска источника ошибки. GHCi предлагает два варианта пошагового выполнения. Используйте :step, чтобы включить все точки останова в программе и выполнять её до достижения следующей точки останова. Используйте :steplocal, чтобы ограничить набор включённых точек останова точками в текущей функции верхнего уровня. Аналогично, используйте :stepmodule, чтобы выполнять пошагово только точки останова в текущем модуле. Например:
*ghci> :step main
Stopped at qsort.hs:5:7-47
_result :: IO ()
Команда :step expr начинает вычисление ⟨expr⟩ в пошаговом режиме. Если ⟨expr⟩ не указан, пошаговое выполнение начинается с текущей точки останова. Команды :steplocal и :stepmodule работают аналогично.
Команда :list особенно полезна при пошаговом выполнении: с её помощью можно увидеть, где именно вы находитесь:
[qsort.hs:5:7-47] *ghci> :list
4
5 main = print (qsort [8, 4, 0, 3, 1, 23, 11, 18])
6
[qsort.hs:5:7-47] *ghci>
GHCi также позволяет выполнять команду при достижении точки останова, поэтому можно настроить автоматический вызов :list:
[qsort.hs:5:7-47] *ghci> :set stop :list
[qsort.hs:5:7-47] *ghci> :step
Stopped at qsort.hs:5:14-46
_result :: [Integer]
4
5 main = print (qsort [8, 4, 0, 3, 1, 23, 11, 18])
6
[qsort.hs:5:14-46] *ghci>
3.5.3. Вложенные точки останова
Когда GHCi остановлен в точке останова и выражение, введённое в приглашении командной строки, приводит к достижению второй точки останова, новая точка становится «текущей», а предыдущая сохраняется в стеке. Таким образом можно создать произвольное количество контекстов точек останова. Например:
[qsort.hs:2:15-46] *ghci> :st qsort [1,3]
Stopped at qsort.hs:(1,0)-(3,55)
_result :: [a]
... [qsort.hs:(1,0)-(3,55)] *ghci>
Остановившись в установленной ранее точке останова на строке 2, мы начали новое вычисление с помощью :step qsort [1,3]. Это новое вычисление остановилось после одного шага (на определении qsort). Приглашение командной строки изменилось: теперь перед ним стоит ..., указывая на то, что помимо текущей есть сохранённые точки останова. Чтобы просмотреть стек контекстов, используйте :show context:
... [qsort.hs:(1,0)-(3,55)] *ghci> :show context
--> main
Stopped at qsort.hs:2:15-46
--> qsort [1,3]
Stopped at qsort.hs:(1,0)-(3,55)
... [qsort.hs:(1,0)-(3,55)] *ghci>
Чтобы прервать текущее вычисление, используйте :abandon:
... [qsort.hs:(1,0)-(3,55)] *ghci> :abandon
[qsort.hs:2:15-46] *ghci> :abandon
*ghci>
3.5.4. Переменная _result
Когда выполнение остановлено в точке останова или при пошаговом выполнении, GHCi связывает переменную _result со значением текущего активного выражения. Значение _result, вероятно, пока недоступно, поскольку вычисление было остановлено, но его можно вычислить принудительно: если тип известен и для него определён вывод, достаточно ввести _result в приглашении командной строки, чтобы увидеть значение. Однако есть один нюанс: вычисление _result, скорее всего, вызовет дополнительные точки останова, начиная с той, в которой мы остановились (если выполнение было остановлено в реальной точке останова, а не командой :step). Поэтому при вычислении _result, вероятно, потребуется сразу же выполнить команду :continue. Также можно воспользоваться командой :force, которая игнорирует точки останова.
3.5.5. Трассировка и история
При отладке программы часто возникает вопрос: «Как я сюда попал?» Традиционные императивные отладчики обычно предоставляют некую возможность трассировки стека, позволяющую увидеть стек активных вызовов функций (иногда называемый «лексическим стеком вызовов»), который описывает путь по коду к текущему месту. К сожалению, в Haskell такую возможность трудно реализовать, поскольку выполнение происходит по требованию, а не в глубину, как в языках со строгими вычислениями. «Стек» в механизме выполнения GHC мало похож на лексический стек вызовов. В идеале GHCi должен поддерживать отдельный лексический стек вызовов наряду с динамическим стеком вызовов; именно так работает наша система профилирования (Профилирование) и некоторые другие отладчики Haskell. Однако пока GHCi не поддерживает лексический стек вызовов (для этого необходимо преодолеть некоторые технические трудности). Вместо этого мы предоставляем возможность вернуться от точки останова к предыдущим шагам вычисления: по сути, это пошаговое выполнение в обратном направлении, которое во многих случаях даёт достаточно информации, чтобы ответить на вопрос «Как я сюда попал?».
Чтобы использовать трассировку, вычислите выражение командой :trace. Например, если установить точку останова на базовом случае qsort:
*ghci> :list qsort
1 qsort [] = []
2 qsort (a:as) = qsort left ++ [a] ++ qsort right
3 where (left,right) = (filter (<=a) as, filter (>a) as)
4
*ghci> :b 1
Breakpoint 1 activated at qsort.hs:1:11-12
*ghci>
а затем запустить небольшое qsort с трассировкой:
*ghci> :trace qsort [3,2,1]
Stopped at qsort.hs:1:11-12
_result :: [a]
[qsort.hs:1:11-12] *ghci>
Теперь можно просмотреть историю шагов вычисления:
[qsort.hs:1:11-12] *ghci> :hist
-1 : qsort.hs:3:24-38
-2 : qsort.hs:3:23-55
-3 : qsort.hs:(1,0)-(3,55)
-4 : qsort.hs:2:15-24
-5 : qsort.hs:2:15-46
-6 : qsort.hs:3:24-38
-7 : qsort.hs:3:23-55
-8 : qsort.hs:(1,0)-(3,55)
-9 : qsort.hs:2:15-24
-10 : qsort.hs:2:15-46
-11 : qsort.hs:3:24-38
-12 : qsort.hs:3:23-55
-13 : qsort.hs:(1,0)-(3,55)
-14 : qsort.hs:2:15-24
-15 : qsort.hs:2:15-46
-16 : qsort.hs:(1,0)-(3,55)
<end of history>
Чтобы изучить один из шагов в истории, используйте :back:
[qsort.hs:1:11-12] *ghci> :back
Logged breakpoint at qsort.hs:3:24-38
_result :: [a]
as :: [a]
a :: a
[-1: qsort.hs:3:24-38] *ghci>
Обратите внимание, что локальные переменные на каждом шаге истории сохранены, и их можно проверять обычным способом. Также обратите внимание, что приглашение командной строки изменилось и указывает на то, что сейчас мы просматриваем первый шаг истории: -1. Для перемещения вперёд по истории можно использовать команду :forward.
Команду :trace можно использовать как с выражением, так и без него. Если выражение не указано, трассировка начинается с текущей точки останова, как и при использовании :step.
История доступна только при использовании :trace; причина в том, что запись каждой точки останова в историю снижает производительность в два раза или больше.
-
-fghci-hist-size=⟨n⟩ -
- По умолчанию:
-
50
Изменяет глубину истории вычислений, отслеживаемой GHCi.
3.5.6. Отладка исключений
Ещё один распространённый вопрос при отладке: «Откуда взялось это исключение?» Исключения, например возникающие при выполнении error или head [], не содержат контекстной информации. Найти конкретный вызов head в программе, приведший к ошибке, может быть кропотливым процессом, обычно включающим Debug.Trace.trace или компиляцию с профилированием и использование Debug.Trace.traceStack либо +RTS -xc (см. -xc).
Отладчик GHCi позволяет быстро разобраться в этих ошибках, не изменяя и не перекомпилируя исходный код. Можно установить точку останова в месте исходного кода, где возникает исключение, а затем использовать :trace и :history, чтобы установить контекст. Однако head находится в библиотеке, и напрямую установить на неё точку останова нельзя. Поэтому в GHCi предусмотрены флаги -fbreak-on-exception, который заставляет вычислитель останавливаться при возникновении исключения, и -fbreak-on-error, работающий аналогично, но останавливающий выполнение только для неперехваченных исключений. При остановке из-за исключения GHCi ведёт себя так же, как при достижении точки останова, но не показывает расположение в исходном коде. Поэтому эти команды действительно полезны только вместе с :trace, чтобы записать шаги, приведшие к исключению. Например:
*ghci> :set -fbreak-on-exception
*ghci> :trace qsort ("abc" ++ undefined)
“Stopped at <exception thrown>
_exception :: e
[<exception thrown>] *ghci> :hist
-1 : qsort.hs:3:24-38
-2 : qsort.hs:3:23-55
-3 : qsort.hs:(1,0)-(3,55)
-4 : qsort.hs:2:15-24
-5 : qsort.hs:2:15-46
-6 : qsort.hs:(1,0)-(3,55)
<end of history>
[<exception thrown>] *ghci> :back
Logged breakpoint at qsort.hs:3:24-38
_result :: [a]
as :: [a]
a :: a
[-1: qsort.hs:3:24-38] *ghci> :force as
*** Exception: Prelude.undefined
[-1: qsort.hs:3:24-38] *ghci> :print as
as = 'b' : 'c' : (_t1::[Char])
Само исключение связывается с новой переменной _exception.
Остановка на исключениях особенно полезна, чтобы выяснить, чем занималась программа во время бесконечного цикла. Нажмите Control-C и изучите историю, чтобы узнать, что происходило.
-
-fbreak-on-exception -
Заставляет GHCi останавливать вычисление и возвращаться к интерактивному приглашению при возникновении исключения. Флаг -fbreak-on-exception останавливает выполнение при любых исключениях.
-
-fbreak-on-error -
Заставляет GHCi останавливать вычисление и возвращаться к интерактивному приглашению при возникновении исключения. Флаг -fbreak-on-error останавливает выполнение только при исключениях, которые в противном случае остались бы неперехваченными.
3.5.7. Пример: проверка функций
С помощью отладчика можно исследовать значения функций. Если мы находимся в точке останова и функция доступна в текущей области видимости, отладчик не может показать её исходный код; однако можно получить некоторую информацию, применив её к аргументам и изучив результат.
Если привязка полиморфна, этот процесс немного усложняется. Рассмотрим пример. Для простоты воспользуемся широко известной функцией map:
import Prelude hiding (map)
map :: (a->b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
Установим точку останова на map и вызовем её.
*ghci> :break 5
Breakpoint 0 activated at map.hs:5:15-28
*ghci> map Just [1..5]
Stopped at map.hs:(4,0)-(5,12)
_result :: [b]
x :: a
f :: a -> b
xs :: [a]
GHCi сообщает, что среди прочих привязок в области видимости находится f. Однако её тип пока известен не полностью, поэтому применить её к каким-либо аргументам нельзя. Тем не менее обратите внимание, что тип её первого аргумента совпадает с типом x, а тип результата совпадает с типом _result.
Как мы уже показали (Точки останова и проверка переменных), в отладчике есть механизм, который уточняет тип f при обнаружении типов x или _result. Поэтому в этой ситуации нужно немного вычислить x, чтобы восстановить и её тип, и часть f, относящуюся к аргументу.
*ghci> seq x ()
*ghci> :print x
x = 1
Теперь можно проверить, что, как и ожидалось, тип x восстановлен, а вместе с ним восстановлен и тип f:
*ghci> :t x
x :: Integer
*ghci> :t f
f :: Integer -> b
Теперь можно применить f к любому аргументу типа Integer и посмотреть на результат.
*ghci> let b = f 10
*ghci> :t b
b :: b
*ghci> b
<interactive>:1:0:
Ambiguous type variable `b' in the constraint:
`Show b' arising from a use of `print' at <interactive>:1:0
*ghci> :p b
b = (_t2::a)
*ghci> seq b ()
()
*ghci> :t b
b :: a
*ghci> :p b
b = Just 10
*ghci> :t b
b :: Maybe Integer
*ghci> :t f
f :: Integer -> Maybe Integer
*ghci> f 20
Just 20
*ghci> map f [1..5]
[Just 1, Just 2, Just 3, Just 4, Just 5]
При первом применении f нам пришлось дополнительно восстановить тип, чтобы узнать тип результата f. После этого можно свободно использовать f обычным образом.
3.5.8. Ограничения
-
Если выполнение остановлено в точке останова и вы попытаетесь вычислить переменную, которая уже вычисляется, второе вычисление зависнет. Это происходит потому, что GHC знает, что переменная уже вычисляется, поэтому новое вычисление ожидает результата, прежде чем продолжить. Но этого, разумеется, не произойдёт, поскольку первое вычисление остановлено в точке останова. Нажатие Control-C прерывает зависшее вычисление и возвращает к приглашению командной строки.
Чаще всего это может произойти, когда вычисляется CAF (например, main), выполнение останавливается в точке останова, а затем в приглашении командной строки снова запрашивается значение CAF.
- Неявные параметры (см. Неявные параметры) доступны в области видимости точки останова только при наличии явной сигнатуры типа.