Как я могу остановить бесконечную оценку в GHCi?

Когда я запускаю что-то вроде:

Prelude> cycle "ab"

Я вижу бесконечную печать "ab". Чтобы остановить это, я просто использую Ctrl + c. И это работает.

Когда я бегу:

Prelude Data.List> nub $ cycle "ab"

Я не могу это остановить.

Вопрос:

  • Почему так?
  • Как я могу остановить эту операцию?

Обновить:

 Ubuntu: version 18.10  
 GHCi:   version 8.2.2

Ответ 1

Отличный вопрос! Тем не менее, поскольку Как прервать выполнение в GHCI? уже фокусируется на вашей второй части, давайте не будем повторять это здесь. Вместо этого позвольте сосредоточиться на первом.

Почему так?

GHC агрессивно оптимизирует циклы. Он оптимизирует их еще больше, если нет выделения, что это даже известная ошибка:

19.2.1. Ошибки в GHC

  • Система выполнения GHC реализует совместную многозадачность, причем переключение контекста может происходить только тогда, когда программа выделяется. Это означает, что программы, которые не выделяются, могут никогда не переключать контекст. Это особенно верно для программ, использующих STM, которые могут зайти в тупик после наблюдения несогласованного состояния. См. TraС# 367 для дальнейшего обсуждения. [акцент мой]

    Если это вас поразило, вы можете скомпилировать затронутый модуль с -fno-omit-yields (см. -f *: независимые от платформы флаги). Этот флаг гарантирует, что точки текучести вставляются в каждую точку входа в функцию (за счет снижения производительности).

Если мы проверим -fomit-yields пропускную -fomit-yields, мы найдем:

-fomit-yields

По умолчанию: очки доходности включены

Говорит GHC пропустить проверку кучи, когда не выполняется выделение. Хотя это улучшает размер двоичных файлов примерно на 5%, это также означает, что потоки, работающие в узких невыделенных циклах, не будут своевременно вытеснены. Если важно всегда иметь возможность прерывать такие потоки, вы должны отключить эту оптимизацию. Также рассмотрите возможность перекомпиляции всех библиотек с отключенной оптимизацией, если вам нужна гарантия прерывистости. [акцент мой]

nub $ cycle "ab" - это узкий, невыделяющий цикл, хотя last $ repeat 1 является еще более очевидным нераспределяющим примером.

"Точки доходности включены" вводят в заблуждение: -fomit-yields по умолчанию включен. Поскольку стандартная библиотека скомпилирована с -fomit-yields, все функции в стандартной библиотеке, которые приводят к узким, невыделенным циклам, могут демонстрировать такое поведение в GHCi, поскольку вы никогда их не перекомпилируете.

Мы можем проверить это с помощью следующей программы:

-- Test.hs
myLast :: [a] -> Maybe a
myLast [x]    = Just x
myLast (_:xs) = myLast xs
myLast _      = Nothing

main = print $ myLast $ repeat 1

Мы можем использовать C-c, чтобы выйти из него, если мы запустим его в GHCi без предварительной компиляции:

$ ghci Test.hs
[1 of 1] Compiling Main             ( Test.hs, interpreted )
Ok, one module loaded.
*Main> :main            <pressing C-c after a while>
Interrupted.

Если мы скомпилируем его и затем повторно запустим в GHCi, он будет зависать:

$ ghc Test.hs
[1 of 1] Compiling Main             ( Test.hs, Test.o )
Linking Test.exe ...

$ ghci Test.hs
Ok, one module loaded.
*Main> :main
<hangs indefinitely>

Обратите внимание, что вам нужно -dynamic если вы не используете Windows, иначе GHCi перекомпилирует исходный файл. Однако, если мы используем -fno-omit-yield, мы внезапно можем снова выйти (в Windows).

Мы можем проверить это еще раз с помощью небольшого фрагмента:

Prelude> last xs = case xs of [x] -> x ; (_:ys) -> last ys
Prelude> last $ repeat 1
^CInterrupted

Поскольку ghci не использует никаких оптимизаций, он также не использует -fomit-yield (и поэтому имеет -fno-omit-yield включен). Наш новый вариант last не приводит к тому же поведению, что и Prelude.last поскольку он не скомпилирован с помощью fomit-yield.

Теперь, когда мы знаем, почему это происходит, мы знаем, что мы будем испытывать такое поведение во всей стандартной библиотеке, поскольку стандартная библиотека компилируется с помощью -fomit-yield.