Spec-Zone.ru › Haskell 9

16. Известные ошибки и неточности

16.1. Стандарты Haskell против Glasgow Haskell: несоответствие языка

В этом разделе перечислены неточности Glasgow Haskell в его реализации Haskell 98 и Haskell 2010. Также см. раздел «Когда возникают проблемы» (Что делать, когда что-то идет не так) для информации о сбоях, утечках памяти и других нежелательных явлениях.

Ограничения здесь перечислены в порядке Haskell Report (приблизительно).

16.1.1. Отклонение от Haskell 98 и Haskell 2010

GHC стремится вести себя (в основном) как компилятор Haskell 98 или Haskell 2010, если вы скажете ему пытаться так себя вести с флагами Haskell98 и Haskell2010. Известные отклонения от стандартов описаны ниже. Если не указано иное, отклонение относится к режиму Haskell 98 и Haskell 2010.

16.1.1.1. Лексическая синтаксис

  • Некоторые лексические правила относительно квалифицированных идентификаторов немного отличаются в GHC по сравнению с отчётом Haskell. Когда у вас есть ⟨module⟩.⟨reservedop⟩, например M.\, GHC будет интерпретировать его как единый квалифицированный оператор вместо двух лексем M и .\.
  • forall всегда является зарезервированным ключевым словом. Это противоречит отчёту Haskell, который допускает использование переменных и типов переменных с именем forall. Обратите внимание, что это не означает, что GHC всегда включает расширение ExplicitForAll. Даже без включённого расширения зарезервирование forall в качестве ключевого слова имеет значение. Например, GHC не будет парсить подпись типа foo :: forall x.
  • Оператор (!), когда он написан в префиксной форме (предваряемый пробелом и не последуемый пробелом, как в f !x = ...), интерпретируется как шаблон bang, в отличие от отчёта Haskell, который предписывает рассматривать ! как оператор независимо от окружающих пробелов. Обратите внимание, что это не означает, что GHC всегда включает BangPatterns. Без расширения GHC выдаст ошибку разбора f !x, попросив включить расширение.
  • Неразрушаемые шаблоны должны быть написаны в префиксной форме:

    f ~a ~b = ...    -- accepted by both GHC and the Haskell Report
    f ~ a ~ b = ...  -- accepted by the Haskell Report but not GHC
    

    Когда они написаны в не-префиксной форме, (~) GHC рассматривает как обычный инфиксный оператор.

    См. Предложение GHC №229 для точных правил.

  • Комментарии строгости в объявлениях данных должны быть написаны в префиксной форме:

    data T = MkT !Int   -- accepted by both GHC and the Haskell Report
    data T = MkT ! Int  -- accepted by the Haskell Report but not GHC
    

    См. Предложение GHC №229 для точных правил.

  • Шаблоны «как» не должны быть окружены пробелами с обеих сторон:

    f p@(x, y, z) = ...    -- accepted by both GHC and the Haskell Report
    
    -- accepted by the Haskell Report but not GHC:
    f p @ (x, y, z) = ...
    f p @(x, y, z) = ...
    f p@ (x, y, z) = ...
    

    Когда они окружены пробелами с обеих сторон, (@) GHC рассматривает как обычный инфиксный оператор.

    Когда предваряются, но не последуют пробелами, (@) рассматривается как видимое применение типа.

    См. Предложение GHC №229 для точных правил.

  • Отчёт Haskell допускает любые десятичные числа Unicode в десятичных литералах. Однако GHC принимает только ASCII-цифры:

    ascDigit    →   0 | 1 | … | 9
    decimal     →   ascDigit {ascDigit}
    
  • GHC более лоялен в выборе допустимых символов в идентификаторах. Другие символы Unicode рассматриваются как строчные буквы, поэтому идентификаторы переменных могут начинаться с них. Класс цифр включает все числа Unicode вместо только десятичных. Модификаторы и знаки без пробелов могут появляться в конце идентификаторов:

    uniSmall    →   any Unicode Lowercase Letter or Other Letter
    uniDigit    →   any Unicode Decimal Number, Letter Number or Other Number
    
    uniIdchar   →   any Unicode Modifier Letter or Non-Spacing Mark
    idchar      →   small | large | digit | uniIdchar | '
    
    varid       →   small {idchar} ⟨reservedid⟩
    conid       →   large {idchar}
    
  • GHC допускает избыточные скобки вокруг имени функции в части funlhs объявлений. То есть GHC успешно распознает объявление вроде ((f)) x = <rhs> для любого количества скобок вокруг f.

16.1.1.2. Контекстно-свободный синтаксис

  • В режиме Haskell 98 (но не в режиме Haskell 2010) GHC немного менее строг к правилу выравнивания при использовании в do выражениях. В частности, ограничение, что «вложенный контекст должен быть отступающим дальше вправо, чем окружающий контекст», ослабляется, чтобы позволить вложенному контексту быть на одном уровне с окружающим контекстом, если вложенный контекст является выражением do.

    Например, следующий код, в котором контекст do вложен в контекст case, а оператор feed animal отступает на такое же значение, как и альтернатива case, принимается GHC:

    main = case animal of
             Wombat -> do
             feed animal
    

    Но этот код с обратным вложением не принимается:

    main = do
      case animal of
      Wombat -> feed animal
    

    Это поведение контролируется расширением NondecreasingIndentation.

NondecreasingIndentation
Since:

7.2.1

Status:

Включено в Haskell98

Разрешить вложенным контекстам иметь тот же уровень отступа, что и окружающий контекст.

  • 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)
    

16.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.

16.1.1.4. Шаблоны с ошибками

После предложения MonadFail Proposal (MFP), блоки do-нотации, содержащие шаблон с ошибками, требуют ограничения MonadFail.

Например

mayFail :: (MonadIO m) => m ()
mayFail = do
  (Just value) <- fetchData
  putStrLn value

Выдаст предупреждение

• Could not deduce (MonadFail m)
    arising from a do statement
    with the failable pattern ‘(Just x)’
  from the context: MonadIO m
    bound by the type signature for:
               mayFail :: forall (m :: * -> *). MonadIO m => m ()

И действительно, так как класс Monad больше не имеет метода fail, нам нужно явно добавить (MonadFail m) к ограничениям функции.

16.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

16.1.1.6. Заголовки модулей по умолчанию с -main-is

Отчёт Haskell2010 указывает в <https://www.haskell.org/onlinereport/haskell2010/haskellch5.html#x11-990005.1>, что

«Допускается сокращённая форма модуля, состоящая только из тела модуля,

в этом случае заголовок предполагается module Main(main) where.

Опция GHC -main-is позволяет изменить имя точки входа верхнего уровня с 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."

16.1.1.7. Система модулей и файлы интерфейса

GHC требует использования файлов hs-boot для разрыва рекурсивных циклов между взаимно рекурсивными модулями, как описано в Взаимно рекурсивные модули и файлы hs-boot. Это скорее недостаток, чем ошибка: в отчёте Haskell говорится (Раздел 5.7)

«В зависимости от используемой реализации Haskell, для отдельной компиляции взаимно рекурсивных модулей может потребоваться, чтобы импортируемые модули содержали дополнительную информацию, чтобы они могли быть обработаны до компиляции. Возможно, потребуются явные типы сигнатур для всех экспортированных значений, чтобы справиться с взаимной рекурсией. Точные детали отдельной компиляции не определены в данном отчёте.»

16.1.1.8. Числа, базовые типы и встроенные классы

Num superclasses

Класс Num не имеет Show или Eq суперклассов.

Вы можете написать код, который работает как с Haskell98/Haskell2010, так и с GHC, если:

  • При создании экземпляра Num типа также создайте экземпляры Show и Eq и
  • При указании функции, экземпляра или класса с ограничением Num t также укажите ограничения Show t и Eq t.
Bits superclass

Класс Bits не имеет суперкласса Num. Следовательно, он не имеет методов по умолчанию для методов bit, testBit и popCount.

Вы можете написать код, работающий как с Haskell 2010, так и с GHC, если:

  • При создании экземпляра Bits типа также создайте экземпляр Num, и
  • При указании функции, экземпляра или класса с ограничением Bits t также укажите ограничение Num t, и
  • Всегда определенйте методы bit, testBit и popCount в экземплярах 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 берёт значение ячейки массива из последней пары (индекс, значение) в списке и не проверяет дубликаты. Причина — эффективность.

16.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.

Это делается для поддержания согласованности с синтаксисом языка. Haskell98 поддерживает шестнадцатеричные и восьмеричные форматы, а GHC2021 — и двоичные.

isAlpha

Определение isAlpha в Haskell 98:

isAlpha c = isUpper c || isLower c

Реализация GHC отличается от определения Haskell 98 в том, что буквенные символы Юникода, которые не являются заглавными или строчными, всё ещё будут считаться буквенными символами функцией isAlpha.

hGetContents

Лень-ввод (Lazy I/O) генерирует исключение при возникновении ошибки, в отличие от спецификации Haskell 98, которая требует игнорирования ошибок (см. раздел 21.2.2 отчёта Haskell 98). Генерируемое исключение — это обычное исключение ввода/вывода, которое было бы сгенерировано при выполнении неудачной операции ввода/вывода в монаде ввода/вывода, и его можно перехватить с помощью System.IO.Error.catch или Control.Exception.catch.

16.1.1.10. Интерфейс внешних функций

hs_init(), hs_exit()

Спецификация FFI требует, чтобы реализация поддерживала повторную инициализацию после завершения работы с hs_exit(), но в настоящее время GHC это не поддерживает. См. #13693.

16.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 (в арифметике со знаком в дополнении до двух с ⟨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 числами не проверяют переполнение, подпотолочение и другие неприятные ситуации. (Обратите внимание, однако, что некоторые архитектуры отлавливают переполнение с плавающей точкой и потерю точности и сообщают об ошибке с плавающей точкой, вероятно, завершая программу).

Поддержка больших кортежей

Отчёт Haskell требует, чтобы реализации предоставляли типы кортежей и соответствующие стандартные экземпляры до размера 15. GHC ограничивает размер типов кортежей до 64 и предоставляет экземпляры Eq, Ord, Bounded, Read, Show и Ix для кортежей до размера 15.

16.2. Известные ошибки или недостатки

Система отслеживания ошибок перечисляет ошибки, которые были сообщены в GHC, но ещё не исправлены: см. систему отслеживания ошибок GHC. В дополнение к этим ошибкам в GHC также есть следующие известные ошибки или недостатки. Эти ошибки более постоянны; маловероятно, что они будут исправлены в ближайшее время.

16.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 расходиться, и исправление проблемы наложило бы дополнительную нагрузку на каждую компиляцию. Поэтому ошибка остаётся неисправленной. Более подробная информация содержится в Секретах инлайнера GHC.

  • На 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 для получения дополнительной информации.

16.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/9.12.1/docs/users_guide/bugs.html

Spec-Zone.ru

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