Spec-Zone.ru › Haskell 8

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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API