17. Известные ошибки и неточности
17.1. Стандарты Haskell против Glasgow Haskell: несоответствие языку
В этом разделе перечислены неточности Glasgow Haskell при реализации Haskell 98 и Haskell 2010. См. также раздел «Что делать, когда что-то идёт не так» (Что делать, когда что-то идёт не так) для информации о сбоях, утечках памяти и других нежелательных явлениях.
Ограничения здесь перечислены в порядке отчёта Haskell (приблизительно).
17.1.1. Отклонения от Haskell 98 и Haskell 2010
По умолчанию GHC в основном стремится вести себя (в основном) как компилятор Haskell 2010, хотя вы можете указать ему вести себя как определённая версия языка с флагами Haskell98 и Haskell2010. Известные отклонения от стандартов описаны ниже. Если не указано иное, отклонение применяется в Haskell 98, Haskell 2010 и по умолчанию.
17.1.1.1. Лексический синтаксис
- Некоторые лексические правила, касающиеся квалифицированных идентификаторов, в GHC немного отличаются от отчёта Haskell. Когда у вас есть ⟨модуль⟩
.⟨зарезервированныйоператор⟩, такой какM.\, GHC будет интерпретировать его как один квалифицированный оператор, а не как две лексемыMи.\. -
forallвсегда является зарезервированным ключевым словом на уровне типов, в отличие от отчёта Haskell, который позволяет именовать переменные типовforall. Обратите внимание, что это не подразумевает, что GHC всегда включает расширениеExplicitForAll. Даже без включения этого расширения резервированиеforallв качестве ключевого слова имеет значение. Например, GHC не будет парсить сигнатуру типаfoo :: forall x.
17.1.1.2. Контекстно-свободный синтаксис
-
В режиме Haskell 98 и по умолчанию (но не в режиме Haskell 2010) GHC немного менее строг относительно правила выравнивания при использовании в
doвыражениях. В частности, ограничение, что «вложенный контекст должен быть выровнен правее, чем окружающий контекст», ослаблено, чтобы позволить вложенному контексту находиться на том же уровне, что и окружающий контекст, если окружающий контекст являетсяdoвыражением.Например, следующий код принимается GHC:
main = do args <- getArgs if null args then return [] else do ps <- mapM process args mapM print psЭто поведение контролируется расширением
NondecreasingIndentation.
-
NondecreasingIndentation -
Разрешить вложенным контекстам находиться на одном уровне выравнивания с окружающим контекстом.
-
GHC не выполняет разрешение фиктивности в выражениях во время парсинга, как требуется в Haskell 98 (но не в Haskell 2010). Например, согласно отчёту Haskell 98, следующее выражение законно:
let x = 42 in x == 42 == True
и парсится как:
(let x = 42 in x == 42) == True
потому что, согласно отчёту, выражение
let«расширяется насколько возможно вправо». Поскольку оно не может расшириться дальше второй знака равенства без вызова ошибки парсинга (==не является фиктивным), выражениеletдолжно завершиться там. GHC просто поглощает всё выражение, парся его так:(let x = 42 in x == 42 == True)
17.1.1.3. Выражения и шаблоны
В своём стандартном режиме GHC делает некоторые программы немного более определёнными, чем они должны быть. Например, рассмотрите
f :: [a] -> b -> b f [] = error "urk" f (x:xs) = \v -> v main = print (f [] `seq` True)
Это должно вызывать error, но фактически выводит True. Причина: GHC эта-расширяет f до
f :: [a] -> b -> b f [] v = error "urk" f (x:xs) v = v
Это немного улучшает эффективность, но существенно для большинства программ, и плохо только для некоторых. Чтобы подавить этот «оптимизм», используйте -fpedantic-bottoms.
17.1.1.4. Объявления и связывания
В своём стандартном режиме GHC не принимает контексты типов данных, так как было принято решение удалить их из следующей версии стандарта языка. Это поведение можно контролировать с помощью расширения DatatypeContexts. См. Контексты типов данных.
17.1.1.5. Проверка типов рекурсивных групп связываний
Отчёт Haskell определяет, что группа связываний (на верхнем уровне или в let или where) должна быть отсортирована по сильно связанным компонентам, а затем проверена на типы в порядке зависимости (Отчёт Haskell, раздел 4.5.1). При проверке типов каждой группы все связующие, имеющие явную сигнатуру типа, помещаются в среду типов с указанным полиморфным типом, а все остальные — мономорфные до обобщения группы (Отчёт Haskell, раздел 4.5.2).
Следуя предложению Марка Джонса в его статье Typing Haskell in Haskell, GHC реализует более общую схему. В GHC анализ зависимостей игнорирует ссылки на переменные, имеющие явную сигнатуру типа. В результате этого усовершенствованного анализа зависимостей группы зависимостей становятся меньше, и больше связываний будет проверяться на типы. Например, рассмотрите:
f :: Eq a => a -> Bool f x = (x == x) || g True || g "Yes" g y = (y <= y) || f True
Это отклоняется Haskell 98, но по схеме Джонса определение для g проверяется на типы сначала, отдельно от определения для f, потому что ссылка на f в правой части g игнорируется анализом зависимостей. Затем тип g обобщается, чтобы получить
g :: Ord a => a -> Bool
Теперь определение для f проверяется на типы с этим типом для g в среде типов.
Тот же усовершенствованный анализ зависимостей также позволяет сигнатурам типов взаимно рекурсивных функций иметь разные контексты, что недопустимо в Haskell 98 (раздел 4.5.2, последнее предложение). GHC требует, чтобы сигнатуры типов уточнённой группы имели одинаковые сигнатуры типов; на практике это означает, что только переменные, связанные одной и той же связью, должны иметь одинаковый контекст. Например, это нормально:
f :: Eq a => a -> Bool f x = (x == x) || g True g :: Ord a => a -> Bool g y = (y <= y) || f True
17.1.1.6. Стандартные заголовки модулей с -main-is
Отчёт Haskell2010 указывает в <https://www.haskell.org/onlinereport/haskell2010/haskellch5.html#x11-990005.1>, что
- «Допускается сокращённая форма модуля, содержащая только тело модуля,
-
в этом случае заголовок предполагается как
module Main(main) where».
Вариант -main-is GHC может использоваться для изменения имени точечной точки входа с main на любую другую переменную. При компиляции основного модуля и при использовании -main-is для переименования точки входа по умолчанию, GHC также будет использовать альтернативное имя в списке экспорта по умолчанию.
Рассмотрим следующую программу:
-- file: Main.hs program :: IO () program = return ()
GHC успешно скомпилирует этот модуль с ghc -main-is Main.program Main.hs, потому что список экспорта по умолчанию будет включать program вместо main, как обычно требует отчёт Haskell.
Это изменение применяется только к главному модулю. Другие модули по-прежнему будут экспортировать main из списка экспорта по умолчанию, независимо от флага -main-is. Это позволяет использовать -main-is с существующими модулями, которые экспортируют main через список экспорта по умолчанию, даже когда -main-is указывает на другую точку входа, как в этом примере (скомпилировано с -main-is MainWrapper.program).
-- file MainWrapper.hs module MainWrapper where import Main program :: IO () program = putStrLn "Redirecting..." >> main -- file Main.hs main :: IO () main = putStrLn "I am main."
17.1.1.7. Система модулей и файлы интерфейсов
GHC требует использования файлов hs-boot для обрыва рекурсивных петель между взаимно рекурсивными модулями, как описано в Как компилировать взаимно рекурсивные модули. Это скорее неточность, чем ошибка: отчёт Haskell гласит (раздел 5.7)
«В зависимости от используемой реализации Haskell, для раздельно-компилируемых взаимно рекурсивных модулей может потребоваться, чтобы импортируемые модули содержали дополнительную информацию, чтобы они могли быть обработаны перед компиляцией. Может потребоваться явные сигнатуры типов для всех экспортированных значений, чтобы справиться с взаимной рекурсией. Точные детали раздельно-компиляции не определяются данным отчётом.»
17.1.1.8. Числа, базовые типы и встроенные классы
-
Num superclasses -
Класс
Numне имеетShowилиEqсуперклассов.Вы можете написать код, работающий как с Haskell98/Haskell2010, так и с GHC, следующим образом:
-
-
Whenever you make a Num instance of a type, also make -
ShowиEqэкземпляры, и
-
-
-
Whenever you give a function, instance or class a Num t -
ограничение, также предоставьте
Show tиEq tограничения.
-
-
-
Bits superclass -
Класс
Bitsне имеетNumсуперкласса. Поэтому у него нет методов по умолчанию для методовbit,testBitиpopCount.Вы можете написать код, работающий как с Haskell 2010, так и с GHC, следующим образом:
-
-
Whenever you make a Bits instance of a type, also make a -
Numэкземпляр, и
-
-
-
Whenever you give a function, instance or class a Bits t -
ограничение, также предоставьте ему
Num tограничение, и
-
-
-
Always define the bit, testBit and popCount methods -
в
Bitsэкземплярах.
-
-
-
Read class methods -
Класс
Readимеет два дополнительных метода,readPrecиreadListPrec, которые отсутствуют в Haskell 2010, так как они полагаются на тип данныхReadPrec, который требует расширениеRankNTypes. GHC также выводитReadэкземпляры, реализуяreadPrecвместоreadsPrec, и опирается на реализацию по умолчаниюreadsPrec, которая определена черезreadPrec. GHC добавляет эти два дополнительных метода просто потому, чтоReadPrecболее эффективен, чемReadS(тип, на котором основанreadsPrec). -
Monad superclass -
Класс
MonadимеетApplicativeсуперкласс. Вы не можете написатьMonadэкземпляры, которые работают для GHC и также для реализации Haskell 2010, которая не определяетApplicative. - Дополнительные экземпляры
-
Определены следующие дополнительные экземпляры:
instance Functor ((->) r) instance Monad ((->) r) instance Functor ((,) a) instance Functor (Either a) instance Monad (Either e)
- Элементы массива с несколькими определениями не проверяются
-
Этот фрагмент кода должен вызывать ошибку, но этого не происходит:
main = print (array (1,1) [(1,2), (1,3)])
Реализация
arrayв GHC берет значение ячейки массива из последней пары (индекс, значение) в списке и не проверяет дубликаты. Причина в эффективности, и только.
17.1.1.9. Поддержка Prelude
-
splitAt semantics -
Data.List.splitAtболее строгий, чем указано в отчете. В частности, отчет указывает, чтоsplitAt n xs = (take n xs, drop n xs)
что подразумевает, что
splitAt undefined undefined = (undefined, undefined)
но реализация GHC строга в своем первом аргументе, поэтому
splitAt undefined [] = undefined
-
Showing records -
Определение
Showв Haskell 2010 устанавливает, что отображаемая строка должна содержать только скобки, которые необходимы для однозначной интерпретации результата. По историческим причинам,Showэкземпляры, полученные от GHC, включают скобки вокруг записей, несмотря на то, что синтаксис записей связывает сильнее, чем применение функции; например,data Hello = Hello { aField :: Int } deriving (Show) -- GHC produces... show (Just (Hello {aField=42})) == "Just (Hello {aField=42})" -- whereas Haskell 2010 calls for... show (Just (Hello {aField=42})) == "Just Hello {aField=42}" -
Reading integers -
Реализация класса
Readв GHC для целочисленных типов принимает шестнадцатеричные и восьмеричные литералы (код в отчете Haskell 98 не делает этого). Так, например,read "0xf00" :: Int
работает в GHC.
Возможная причина в том, что
readLitCharпринимает шестнадцатеричные и восьмеричные экранирования, поэтому кажется нелогичным не делать этого и для целых чисел. -
isAlpha -
Определение
isAlphaв Haskell 98:isAlpha c = isUpper c || isLower c
Реализация GHC отклоняется от определения Haskell 98 в том смысле, что алфавитно-цифровые символы Unicode, которые не являются заглавными или строчными, все равно будут распознаваться как алфавитно-цифровые символы
isAlpha. -
hGetContents -
Ленивые ввод/вывод выбрасывают исключение, если возникает ошибка, в отличие от спецификации Haskell 98, которая требует, чтобы ошибки игнорировались (см. раздел 21.2.2 отчета Haskell 98). Выбрасываемое исключение — это обычное исключение ввода/вывода, которое было бы выброшено, если бы неудачная операция ввода/вывода выполнялась в монаде IO, и его можно перехватить с помощью
System.IO.Error.catchилиControl.Exception.catch.
17.1.1.10. Интерфейс внешних функций
-
hs_init(), hs_exit() -
Спецификация FFI требует, чтобы реализация поддерживала повторную инициализацию после остановки с помощью
hs_exit(), но GHC в настоящее время этого не поддерживает. См. #13693.
17.1.1.11. Секции операторов
Отчет Haskell требует, чтобы для инфиксных операторов %, соблюдались следующие тождества:
(% expr) = \x -> x % expr (expr %) = \x -> expr % x
Однако, второе тождество нарушается в присутствии неопределенных операторов,
(%) = error "urk" (() %) `seq` () -- urk (\x -> () % x) `seq` () -- OK, result ()
Секция оператора обрабатывается как применение функции неопределенной функции, в то время как форма лямбда находится в WHNF, которая содержит применение неопределенной функции.
17.1.2. Интерпретация GHC неопределенного поведения в Haskell 98 и Haskell 2010
В этом разделе описана позиция GHC по различным вопросам, которые не определены или зависят от реализации в Haskell 98.
-
Char -
В соответствии со стандартом ISO-10646,
maxBound :: Charв GHC — это0x10FFFF. -
Int -
В GHC тип
Intсоответствует размеру адреса на архитектуре хоста; другими словами, он содержит 32 бита на 32-битной машине и 64 бита на 64-битной машине.Арифметические операции с
Intне проверяются на переполнениеInt, поэтому все операции сIntвыполняются по модулю 2⟨n⟩, где ⟨n⟩ — размер в битах типаInt.Тип
fromInteger(и, следовательно, такжеfromIntegral) является особым случаем при преобразовании вInt. ЗначениеfromIntegral x :: Intопределяется путем взятия младших ⟨n⟩ битов(abs x), умноженных на знакx(в арифметике со знаком дополнения до 2 на ⟨n⟩ битов). Это поведение было выбрано для того, чтобы, например, запись0xffffffff :: Intсохраняла битовую структуру в результирующемInt.Отрицательные литералы, такие как
-3, согласно (внимательному прочтению) отчету Haskell, означаютPrelude.negate (Prelude.fromInteger 3). Таким образом,-2147483648означаетnegate (fromInteger 2147483648). ПосколькуfromIntegerберет младшие 32 бита представления,fromInteger (2147483648::Integer), вычисленное в типеInt— это-2147483648::Int.Затем операция
negateпереполняется, но она не проверяется, поэтомуnegate (-2147483648::Int)— это просто-2147483648. Короче говоря, можно записатьminBound::Intкак литерал со значением, которое ожидается (но это не гарантируется в общем случае).Функция
fromIntegralтакже сохраняет битовую структуру при преобразовании между размерными целочисленными типами (Int8,Int16,Int32,Int64и беззнаковыми вариантамиWord), см. модулиData.IntиData.Wordв документации библиотеки. - Арифметика с плавающей запятой без проверок
-
Операции над числами с плавающей запятой
FloatиDoubleне проверяются на переполнение, недополнение и другие неприятности. (Обратите внимание, однако, что некоторые архитектуры отлавливают переполнение и потерю точности с плавающей запятой и сообщают об исключении с плавающей запятой, вероятно, завершая программу) -
Large tuple support -
Отчет Haskell требует только, чтобы реализации предоставляли типы кортежей и их соответствующие стандартные экземпляры до размера 15. GHC ограничивает размер типов кортежей до 62 и предоставляет экземпляры
Eq,Ord,Bounded,Read, иShowдля кортежей до размера 15. Однако,Ixэкземпляры предоставляются только для кортежей до размера 5.
17.2. Известные ошибки или неточности
В системе отслеживания ошибок перечислены ошибки, которые были сообщены в GHC, но еще не исправлены: см. систему отслеживания ошибок GHC. В дополнение к этим ошибкам GHC также имеет следующие известные ошибки или неточности. Эти ошибки более стойкие; маловероятно, что какая-либо из них будет исправлена в ближайшее время.
17.2.1. Ошибки в GHC
-
Система выполнения GHC реализует кооперативную многозадачность, при этом переключение контекста может происходить только при выделении памяти программой. Это означает, что программы, которые не выделяют память, никогда не будут переключать контекст. Это особенно верно для программ, использующих STM, которые могут заблокироваться после наблюдения за несогласованным состоянием. См. #367 для дальнейшего обсуждения.
Если вы столкнулись с этой проблемой, возможно, вам следует скомпилировать затронутый модуль с помощью
-fno-omit-yields(см. -f*: независимые от платформы флаги). Этот флаг гарантирует, что точки возврата будут вставлены в каждой точке входа функции (за счёт некоторой производительности). -
GHC не позволяет вам иметь тип данных с контекстом, который упоминает переменные типов, которые не являются параметрами типа данных. Например:
data C a b => T a = MkT a
так что тип
MkTравенMkT :: forall a b. C a b => a -> T a
В принципе, с подходящим объявлением класса с функциональной зависимостью, возможно, что этот тип не является неоднозначным; но GHC тем не менее его отклоняет. Переменные типов, упомянутые в контексте объявления типа данных, должны быть среди параметров типа данных.
-
Инлайнер GHC может быть введён в бесконечную рекурсию стандартным способом кодирования рекурсии через тип данных:
data U = MkU (U -> Bool) russel :: U -> Bool russel u@(MkU p) = not $ p u x :: Bool x = russel (MkU russel)
Бесконечная рекурсия сообщается следующим образом:
ghc: panic! (the 'impossible' happened) (GHC version 8.2.1 for x86_64-unknown-linux): Simplifier ticks exhausted When trying UnfoldingDone x_alB To increase the limit, use -fsimpl-tick-factor=N (default 100)причём паника сообщается вне зависимости от того, насколько высокий
-fsimpl-tick-factorвы укажете.Мы никогда не находили другого класса программ, кроме этого искусственного, который заставлял бы GHC расходиться, и исправление проблемы наложило бы дополнительную нагрузку на каждую компиляцию. Поэтому ошибка остаётся не исправленной. Более подробную информацию можно найти в Secrets of the GHC inliner.
-
На 32-битных платформах x86 при использовании нативного генератора кода опция
-fexcess-precisionвсегда включена. Это означает, что вычисления с плавающей точкой не являются детерминированными, потому что в зависимости от того, как программа компилируется (например, настройки оптимизации), некоторые вычисления могут выполняться с точностью 80 бит вместо предполагаемой 32-битной или 64-битной точности. Результаты вычислений с плавающей точкой могут отличаться при включённой оптимизации. В худшем случае нарушается референциальная прозрачность, потому что, например,let x = E1 in E2может вычисляться к другому значению, чемE2[E1/x].Одним из решений является использование опции
-msse2(см. Платформенно-специфические флаги), которая генерирует код для использования набора инструкций SSE2 вместо набора инструкций x87. Код SSE2 использует правильную точность для всех операций с плавающей точкой и, следовательно, даёт детерминированные результаты. Однако обратите внимание, что это работает только с процессорами, поддерживающими SSE2 (Intel Pentium 4 или AMD Athlon 64 и более поздние), поэтому опция по умолчанию не включена. Библиотеки, поставляемые с GHC, вероятно, были скомпилированы без этой опции, если вы не скомпилировали GHC сами. -
Оптимизация
state hackможет привести к неявным изменениям в порядке вычисления, которые могут скрывать исключения, даже с-fpedantic-bottoms(см., например, #7411). Например,import Control.Exception import Control.DeepSeq main = do evaluate (('a' : undefined) `deepseq` return () :: IO ()) putStrLn "Hello"Компиляция этой программы с
-Oприводит к тому, что выводитсяHello, несмотря на то, чтоevaluateдолжно было завершиться ошибкой. Компиляция с-O -fno-state-hackприводит к ожидаемой ошибке. -
Программы, скомпилированные с
-fdefer-type-errors, могут немного раньше завершаться ошибкой, чем можно ожидать. Например,{-# OPTIONS_GHC -fdefer-type-errors #-} main = do putStrLn "Hi there." putStrLn TrueНе выведет ничего, несмотря на то, что плохо типизированный терм появляется после хорошо типизированного
putStrLn "Hi there.". См. #11197. - Несмотря на внешнее представление
*иConstraintна самом деле не являются различными типами в внутренней представлении компилятора и могут быть объединены, что приведёт к неожиданным результатам. См. #11715 для одного примера. - Из-за ограничения инструментальной цепочки мы не можем поддерживать полные пути Unicode на Windows. На Windows мы поддерживаем до Latin-1. См. #12971 для получения дополнительной информации.
17.2.2. Ошибки в GHCi (интерактивный GHC)
-
GHCi не учитывает объявление
defaultв модуле, область которого вы используете. Вместо этого для выражений, введённых в командной строке, всегда используется поведение по умолчанию; то есть,default(Int,Double).Было бы лучше, если бы GHCi записывал, какими являются настройки по умолчанию в каждом модуле, и использовал те, которые относятся к «текущему» модулю (каким бы он ни был).
-
На Windows существует ошибка GNU ld/BFD, в результате которой генерируются поддельные PE-объектные файлы с количеством ссылок, превышающим 0xffff. Когда GHCi пытается загрузить пакет, затронутый этой ошибкой, вы получаете сообщение об ошибке следующего вида
Loading package javavm ... linking ... WARNING: Overflown relocation field (# relocs found: 30765)
В последний раз, когда мы проверяли, эта ошибка всё ещё не была исправлена в коде BFD, и в то время как мы сообщали об ошибке, в 2001 году или около того, никакого заметного интереса к её исправлению не было.
Решение — разделить файлы .o, составляющие ваш пакет, на два или более .o, подобно тому, как это делает пакет
base.
© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/8.10.2/docs/html/users_guide/bugs.html