Spec-Zone.ru › Haskell 9

5.8. Имена файлов и раздельная компиляция

В этом разделе описано, какие файлы GHC ожидает найти, какие файлы он создаёт, где они хранятся и какие параметры влияют на это поведение.

Правила именования путей различаются в разных системах. В частности, в Unix системах в качестве разделителя каталогов используется «/», а в Windows системах — «\». В дальнейших разделах мы будем последовательно использовать «/» в качестве разделителя каталогов; замените его на соответствующий символ в вашей системе.

5.8.1. Исходные файлы Haskell

Каждый исходный модуль Haskell следует помещать в отдельный файл.

Как правило, файл следует называть по имени модуля, заменяя точки в имени модуля разделителями каталогов. Например, в системе Unix модуль A.B.C следует поместить в файл A/B/C.hs относительно некоторого базового каталога. Если модуль не будет импортироваться другим модулем (например, Main), для него можно использовать любое имя файла.

GHC предполагает, что исходные файлы содержат только символы ASCII или UTF-8; другие кодировки не распознаются. Однако некорректные последовательности UTF-8 игнорируются в комментариях, поэтому можно использовать другие кодировки, например Latin-1, если исходный код вне комментариев содержит только символы ASCII.

5.8.2. Выходные файлы

При компиляции исходного файла GHC обычно создаёт два файла: объектный файл и файл интерфейса.

Объектный файл, имя которого обычно заканчивается суффиксом .o, содержит скомпилированный код модуля.

Файл интерфейса, имя которого обычно заканчивается суффиксом .hi, содержит информацию, необходимую GHC для компиляции других модулей, зависящих от этого модуля. В нём содержатся, например, типы экспортируемых функций, определения типов данных и так далее. Он хранится в двоичном формате, поэтому не пытайтесь его читать; вместо этого используйте параметр --show-iface ⟨file⟩ (см. раздел Другие параметры, связанные с файлами интерфейсов).

Объектный файл и файл интерфейса следует рассматривать как пару, поскольку файл интерфейса — это, в некотором смысле, описание содержимого объектного файла, предназначенное для чтения компилятором. Если файл интерфейса и объектный файл по какой-либо причине рассинхронизируются, компилятор может сделать неверные предположения об объектном файле; почти наверняка это приведёт к проблемам. Поэтому мы рекомендуем хранить объектные файлы и файлы интерфейсов в одном месте (по умолчанию GHC так и делает, однако, как мы вскоре объясним, значения по умолчанию можно переопределить).

У каждого модуля есть имя модуля, определённое в его исходном коде (module A.B.C where ...).

Имя объектного файла, создаваемого GHC, определяется по следующим правилам, где ⟨osuf⟩ — суффикс объектного файла (его можно изменить с помощью параметра -osuf).

  • Если параметр -odir не задан (значение по умолчанию), имя объектного файла определяется по имени исходного файла (имя модуля не учитывается), а его суффикс заменяется на ⟨osuf⟩.
  • Если задан параметр -odir ⟨dir⟩, имя объектного файла имеет вид ⟨dir⟩/⟨mod⟩.⟨osuf⟩, где ⟨mod⟩ — имя модуля, в котором точки заменены косыми чертами. Если необходимая структура каталогов внутри ⟨dir⟩ ещё не существует, GHC создаст её без уведомления.

Имя файла интерфейса определяется по тем же правилам, за исключением того, что вместо ⟨osuf⟩ используется суффикс ⟨hisuf⟩ (по умолчанию .hi), а вместо параметров -odir ⟨dir⟩ и -osuf ⟨suffix⟩ используются соответствующие параметры -hidir ⟨dir⟩ и -hisuf ⟨suffix⟩.

Например, если GHC компилирует модуль A.B.C из файла src/A/B/C.hs без флагов -odir и -hidir, файл интерфейса будет помещён в src/A/B/C.hi, а объектный файл — в src/A/B/C.o.

Для любого импортируемого модуля имя модуля в инструкции импорта должно в точности совпадать с именем модуля в файле интерфейса (или исходном файле), найденном согласно стратегии, описанной в разделе Путь поиска. Это означает, что для большинства модулей имя исходного файла должно совпадать с именем модуля.

Однако вполне допустимо поместить модуль Main в файл с именем foo.hs, но это работает только потому, что GHC никогда не требуется искать интерфейс модуля Main (поскольку он никогда не импортируется). Поэтому в одном каталоге можно иметь несколько модулей Main в отдельных исходных файлах, и GHC не будет путаться.

В режиме пакетной компиляции имя объектного файла также можно переопределить с помощью параметра -o ⟨file⟩, а имя файла интерфейса можно задать напрямую с помощью параметра -ohi ⟨file⟩.

5.8.3. Путь поиска

В программе модуль Foo импортируется с помощью инструкции import Foo. В режиме --make или в GHCi GHC найдёт исходный файл для Foo и сначала скомпилирует его. Без --make GHC будет искать файл интерфейса для Foo, который должен быть создан при предыдущей компиляции Foo.

Поиск исходных файлов выполняется следующим образом: GHC хранит список каталогов, называемый путём поиска. Для каждого из этих каталогов он пытается добавить к нему ⟨basename⟩.⟨extension⟩ и проверяет, существует ли полученный файл. Значение ⟨basename⟩ — это имя модуля, в котором точки заменены разделителем каталогов («/» или «\\"» — в зависимости от системы), а ⟨extension⟩ — расширение исходного файла (hs, lhs), если используется режим --make или GHCi.

При поиске файлов интерфейсов в режиме -c поиск выполняется в каталоге -hidir, если он задан. В противном случае для поиска файла интерфейса используется та же стратегия, что и для исходных файлов.

Например, пусть путь поиска содержит каталоги d1, d2 и d3, а мы ищем исходный файл модуля A.B.C в режиме --make. GHC будет искать его в d1/A/B/C.hs, d1/A/B/C.lhs, d2/A/B/C.hs и так далее.

По умолчанию путь поиска содержит один каталог: «.» (то есть текущий каталог). Для добавления каталогов в путь поиска или изменения его содержимого можно использовать следующие параметры:

-i⟨dir⟩[:⟨dir⟩]*

Этот флаг добавляет в путь поиска список каталогов dirs, разделённых двоеточиями.

-i

сбрасывает путь поиска, оставляя его пустым.

Это ещё не всё: GHC также ищет модули в предварительно скомпилированных библиотеках, называемых пакетами. Подробности см. в разделе о пакетах (Пакеты).

5.8.4. Перенаправление выходных файлов компиляции

-o ⟨file⟩

Скомпилированный результат GHC обычно записывается в файл .hc, .o и т. д. в зависимости от последней стадии компиляции. Параметр -o file перенаправляет вывод этой последней стадии в ⟨file⟩.

Примечание

Эта «возможность» может показаться нелогичной: ghc -C -o foo.o foo.hs поместит промежуточный код C в файл foo.o, несмотря на его имя!

Чаще всего этот параметр используется при создании исполняемого файла, чтобы задать его имя. Например:

ghc -o prog --make Main

скомпилирует программу, начиная с модуля Main, и поместит исполняемый файл в prog.

Примечание: в Windows, если результатом является исполняемый файл, к указанному имени добавляется расширение «.exe», если у него ещё нет расширения. Таким образом,

ghc -o foo Main.hs

скомпилирует и скомпонует модуль Main.hs и поместит полученный исполняемый файл в foo.exe (а не в foo).

Если вы используете ghc --make, но не используете -o, имя исполняемого файла, выбранное GHC, будет основано на имени файла, содержащего модуль Main. Обратите внимание, что в GHC модуль Main не обязательно должен находиться в файле Main.hs. Поэтому обе команды

ghc --make Prog

и

ghc --make Prog.hs

создадут файл Prog (или Prog.exe в Windows).

-dyno ⟨file⟩

При использовании -dynamic-too параметр -dyno ⟨suffix⟩ является аналогом -o. Он перенаправляет динамический вывод в ⟨file⟩.

-odir ⟨dir⟩

Перенаправляет объектные файлы в каталог ⟨dir⟩. Например:

$ ghc -c parse/Foo.hs parse/Bar.hs gurgle/Bumble.hs -odir `uname -m`

Объектные файлы Foo.o, Bar.o и Bumble.o будут помещены в подкаталог, названный в соответствии с архитектурой выполняющей машины (x86, mips и т. д.).

Обратите внимание, что параметр -odir не влияет на каталог, в который помещаются файлы интерфейсов; для этого используйте параметр -hidir. В приведённом выше примере они по-прежнему будут помещены в parse/Foo.hi, parse/Bar.hi и gurgle/Bumble.hi.

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

-ohi ⟨file⟩

Параметр -ohi bar2/Wurble.iface позволяет перенаправить вывод файла интерфейса в другой файл bar2/Wurble.iface (не рекомендуется).

Предупреждение

Если перенаправить файл интерфейса туда, где GHC не сможет его найти, проверка необходимости перекомпиляции может работать некорректно (как минимум, перекомпиляции избежать не удастся). По возможности рекомендуем вместо этого использовать комбинацию параметров -hidir и -hisuf.

Чтобы вообще не создавать файл интерфейса, можно перенаправить его в никуда с помощью этого параметра, например: -ohi /dev/null.

-dynohi ⟨file⟩

При использовании -dynamic-too параметр -dynohi ⟨file⟩ является аналогом -ohi. Он перенаправляет вывод динамического файла интерфейса в ⟨file⟩.

-hidir ⟨dir⟩

Перенаправляет все создаваемые файлы интерфейсов в каталог ⟨dir⟩ вместо каталога по умолчанию.

Также обратите внимание, что при инкрементальной компиляции (с помощью ghc --make или ghc -c) GHC ищет файлы интерфейсов именно в этом каталоге.

-hiedir ⟨dir⟩

Перенаправляет все создаваемые расширенные файлы интерфейсов в каталог ⟨dir⟩ вместо каталога по умолчанию.

Также обратите внимание, что при инкрементальной компиляции (с помощью ghc --make или ghc -c) GHC ищет расширенные файлы интерфейсов именно в этом каталоге.

-stubdir ⟨dir⟩

Перенаправляет все создаваемые файлы-заглушки FFI в каталог ⟨dir⟩. Файлы-заглушки создаются, если исходный код Haskell содержит объявление foreign export или foreign import "&wrapper" (см. раздел Использование foreign export и foreign import ccall "wrapper" с GHC). Параметр -stubdir работает с иерархическими модулями точно так же, как -odir и -hidir.

-dumpdir ⟨dir⟩

Перенаправляет все файлы дампа в каталог ⟨dir⟩. Файлы дампа создаются, если -ddump-to-file используется с другими флагами -ddump-*.

-outputdir ⟨dir⟩

Параметр -outputdir является сокращённой записью для комбинации параметров -odir ⟨dir⟩, -hidir ⟨dir⟩, -hiedir ⟨dir⟩, -stubdir ⟨dir⟩ и -dumpdir ⟨dir⟩.

-osuf ⟨suffix⟩

Параметр -osuf ⟨suffix⟩ изменяет суффикс .o объектных файлов на указанный вами. Мы используем его при компиляции библиотек, чтобы объектные файлы профилируемых версий библиотек не перезаписывали обычные.

-dynosuf ⟨suffix⟩

При использовании -dynamic-too параметр -dynosuf ⟨suffix⟩ является аналогом -osuf. Он изменяет суффикс .dyn_o динамических объектных файлов.

-hisuf ⟨suffix⟩

Аналогично, параметр -hisuf ⟨suffix⟩ изменяет суффикс .hi файлов интерфейсов, не относящихся к системным (см. раздел Другие параметры, связанные с файлами интерфейсов).

Сочетание -hisuf/-osuf особенно полезно, если нужно скомпилировать программу в одном каталоге как с профилированием, так и без него. Можно выполнить:

ghc ...

чтобы получить обычную версию, и

ghc ... -osuf prof.o -hisuf prof.hi -prof -fprof-auto

чтобы получить профилируемую версию.

-dynhisuf ⟨suffix⟩

При использовании -dynamic-too параметр -dynhisuf ⟨suffix⟩ является аналогом -hisuf. Он изменяет суффикс .dyn_hi динамических файлов интерфейсов.

-hiesuf ⟨suffix⟩

Параметр -hiesuf ⟨suffix⟩ изменяет суффикс .hie расширенных файлов интерфейсов на указанный вами.

-hcsuf ⟨suffix⟩

Наконец, параметр -hcsuf ⟨suffix⟩ изменяет суффикс .hc промежуточных файлов C, создаваемых компилятором.

5.8.5. Сохранение промежуточных файлов

Следующие параметры позволяют сохранять (или не сохранять) некоторые промежуточные файлы, которые GHC обычно удаляет после компиляции:

-keep-hc-file
-keep-hc-files

Сохраняет промежуточные файлы .hc при компиляции из .hs в .o через C (примечание: файлы .hc создаются только компиляторами без регистрации).

-keep-hi-files

Сохраняет промежуточные файлы .hi. Это поведение установлено по умолчанию. Используйте -no-keep-hi-files, если файлы .hi вам не нужны.

-keep-hscpp-file
-keep-hscpp-files

Сохраняет результат этапа предварительной обработки CPP в виде файлов .hscpp. Файл .hscpp создаётся только при компиляции модуля, использующего препроцессор C.

-keep-llvm-file
-keep-llvm-files
Подразумевает:

-fllvm

Сохраняет промежуточные файлы .ll при компиляции из .hs в .o через LLVM (примечание: файлы .ll не создаются при использовании собственного генератора машинного кода; для их создания может потребоваться параметр -fllvm).

-keep-o-files

Сохраняет промежуточные файлы .o. Это поведение установлено по умолчанию. Используйте -no-keep-o-files, если файлы .o вам не нужны.

-keep-s-file
-keep-s-files

Сохраняет промежуточные файлы .s.

-keep-tmp-files

Указывает драйверу GHC не удалять временные файлы, которые обычно хранятся в /tmp (или, возможно, в другом месте; см. раздел Перенаправление временных файлов). Запуск GHC с параметром -v покажет, какие временные файлы были созданы в процессе работы.

5.8.6. Перенаправление временных файлов

-tmpdir ⟨dir⟩

Если возникают проблемы из-за нехватки места в /tmp (или другом каталоге, куда ваша система помещает временные файлы), можно использовать параметр -tmpdir ⟨dir⟩, чтобы указать другой каталог. Например, -tmpdir . задаёт текущий рабочий каталог для временных файлов.

В качестве альтернативы используйте переменную окружения TMPDIR. Укажите в ней имя каталога, куда следует помещать временные файлы. GCC и другие программы также учитывают переменную TMPDIR.

5.8.7. Другие параметры, связанные с файлами интерфейсов

-ddump-hi

Выводит новый файл интерфейса в стандартный поток вывода.

-ddump-hi-diffs

Если новый файл интерфейса .hi совпадает со старым, компилятор не перезаписывает существующий файл; это удобно при использовании make. Если же интерфейс изменился, зачастую полезно узнать об этом. Параметр -ddump-hi-diffs заставляет GHC сообщать о различиях между старым и новым файлами .hi.

-ddump-minimal-imports

Записывает в файл M.imports (где ⟨M⟩ — имя компилируемого модуля) «минимальный» набор объявлений импорта. Каталог, в котором создаются файлы .imports, можно задать с помощью параметра -dumpdir ⟨dir⟩.

Все объявления импорта в M.hs можно заменить соответствующими объявлениями из файла .imports. Зачем это делать? Потому что «минимальные» импорты (а) явно импортируют всё по имени и (б) не импортируют ничего лишнего. Поддерживать это свойство вручную довольно сложно, поэтому этот флаг призван упростить задачу.

--show-iface ⟨file⟩

где ⟨file⟩ — имя файла интерфейса; выводит его содержимое в удобочитаемом формате. См. раздел Режимы работы.

5.8.8. Параметры, связанные с расширенными файлами интерфейсов

При компиляции исходного файла Haskell GHC собирает о нём большой объём информации. Расширенные файлы интерфейсов позволяют сохранять часть этой информации на диске, чтобы внешние инструменты, например IDE, могли не выполнять повторный разбор, проверку типов и переименование. Эти файлы содержат

  • упрощённое AST

    • узлы аннотированы исходными позициями и типами
    • идентификаторы аннотированы информацией об области видимости
  • необработанные байты исходного кода Haskell

GHC API предоставляет функции для чтения и записи этих файлов.

-fwrite-ide-info

Записывает расширенные файлы интерфейсов рядом с обычными файлами интерфейсов. Как и для обычных файлов интерфейсов, GHC проверяет, не устарели ли расширенные файлы интерфейсов и существуют ли они.

-fvalidate-ide-info

Выполняет ряд проверок и статический анализ создаваемых расширенных файлов интерфейсов. В частности, проверяется, например, что переменные не используются за пределами ожидаемых областей видимости.

Из-за формата, в котором GHC сейчас хранит AST с проверенными типами, сбор типов для некоторых узлов выражений обходится дорого. Для повышения производительности GHC пока пропускает их, поэтому не следует ожидать, что информация о типах будет доступна для всех узлов выражений. Подробнее см. в задаче #16233.

5.8.9. Проверка необходимости перекомпиляции

-fforce-recomp

Отключает проверку необходимости перекомпиляции (по умолчанию она включена). Обычно такая проверка позволяет досрочно завершить компиляцию и оставить существующий файл .o на месте, если удаётся определить, что модуль не нужно перекомпилировать.

-fignore-optim-changes
-fignore-hpc-changes

Раньше GHC сравнивал только что созданный файл .hi с предыдущей версией; если они совпадали, он оставлял старый файл без изменений и не менял дату его модификации. В результате модули, импортирующие модуль с неизменившимся выходным файлом .hi, не перекомпилировались.

Теперь этот подход не работает. Предположим, что модуль C импортирует модуль B, а B импортирует модуль A. Поэтому изменения в модуле A могут потребовать перекомпиляции модуля C, а значит, при изменении A.hi нужно проверять, следует ли перекомпилировать C. Однако в списке зависимостей C указан только B.hi, но не A.hi, а некоторые изменения в A (например, изменение определения функции, используемой при встраивании функции, экспортируемой B) теоретически могут не изменить B.hi ни на йоту. Поэтому теперь…

GHC вычисляет контрольную сумму (фактически хеш MD5) для каждого файла интерфейса и каждого объявления в нём. Кроме того, в каждом файле интерфейса хранится список контрольных сумм всего, что использовалось при последней компиляции файла. Если хеш MD5 исходного файла, сохранённый в файле .hi, не изменился, дата модификации файла .o не меньше даты модификации файла .hi и проверка перекомпиляции включена, GHC действует предусмотрительно. Он сравнивает контрольные суммы необходимых сейчас данных с контрольными суммами данных, необходимых при предыдущей компиляции (полученными из файла интерфейса компилируемого модуля); если они все совпадают, компиляция досрочно останавливается с сообщением «Компиляция НЕ ТРЕБУЕТСЯ». Какое прекрасное зрелище!

О том, как всё это работает, можно прочитать в описании внутреннего устройства GHC.

5.8.9.1. Перекомпиляция для Template Haskell и плагинов

При использовании Template Haskell или плагинов проверка необходимости перекомпиляции становится несколько сложнее. Обе эти возможности выполняют код во время компиляции, поэтому изменение любого из фрагментов выполняемого кода требует перекомпиляции модуля. Рассмотрим сплайс верхнего уровня:

main = $(foo bar [| () |])

При компиляции модуля будет вычислено foo bar [| () |], а полученный код будет включён в программу. Зависимости выражения вычисляются и сохраняются во время компиляции модуля. При записи файла интерфейса создаются дополнительные зависимости от объектных файлов, от которых зависит выражение. Например, если foo находится в модуле A, а bar — в модуле B, модуль будет зависеть от A.o и B.o; при изменении любого из них модуль будет перекомпилирован.

5.8.10. Взаимно рекурсивные модули и файлы hs-boot

GHC поддерживает компиляцию взаимно рекурсивных модулей. В этом разделе объясняется, как это работает.

Каждый цикл в графе импорта модулей необходимо разорвать с помощью файла hs-boot. Предположим, что модули A.hs и B.hs являются исходными файлами Haskell, например:

module A where
    import B( TB(..) )

    newtype TA = MkTA Int

    f :: TB -> TA
    f (MkTB x) = MkTA x

module B where
    import {-# SOURCE #-} A( TA(..) )

    data TB = MkTB !Int

    g :: TA -> TB
    g (MkTA x) = MkTB x

Здесь A импортирует B, но B импортирует A с прагмой {-# SOURCE #-}, которая разрывает циклическую зависимость. Каждый цикл в графе импорта модулей необходимо разорвать импортом {-# SOURCE #-}; или, что эквивалентно, граф импорта модулей должен быть ацикличным, если игнорировать импорты {-# SOURCE #-}.

Для каждого модуля A.hs, импортируемого таким образом через {-# SOURCE #-}, должен существовать исходный файл A.hs-boot. Этот файл содержит сокращённую версию A.hs, например:

module A where
    newtype TA = MkTA Int

Чтобы скомпилировать эти три файла, выполните следующие команды:

ghc -c A.hs-boot    -- Produces A.hi-boot, A.o-boot
ghc -c B.hs         -- Consumes A.hi-boot, produces B.hi, B.o
ghc -c A.hs         -- Consumes B.hi, produces A.hi, A.o
ghc -o foo A.o B.o  -- Linking the program

Здесь следует отметить несколько моментов:

  • Файл A.hs-boot — это исходный файл, написанный программистом. Он должен находиться в том же каталоге, что и его родительский исходный файл A.hs. В настоящее время при использовании литературного исходного файла A.lhs необходимо также использовать литературный загрузочный файл A.lhs-boot, и наоборот.
  • Файл hs-boot компилируется GHC так же, как файл hs:

    ghc -c A.hs-boot
    

    При компиляции файла hs-boot A.hs-boot проверяются ошибки области видимости и типов. При компиляции его родительского модуля A.hs эти два файла сравниваются, и если они несовместимы, сообщается об ошибке.

  • Подобно тому как компиляция A.hs создаёт файл интерфейса A.hi и объектный файл A.o, компиляция A.hs-boot создаёт файл интерфейса A.hi-boot и псевдообъектный файл A.o-boot:

    • Псевдообъектный файл A.o-boot пуст (не связывайте его!), но он очень полезен при использовании Makefile, поскольку позволяет отслеживать, когда файл A.hi-boot в последний раз обновлялся (см. Использование make).
    • Файл hi-boot, созданный при компиляции файла hs-boot, имеет тот же машинно-генерируемый двоичный формат, что и любой другой файл интерфейса, создаваемый GHC (например, B.hi). Его содержимое можно отобразить с помощью ghc --show-iface. Если задан каталог для файлов интерфейса с помощью флага -hidir, это также повлияет на файлы hi-boot.
  • Если считать файлы hs-boot отдельными от их родительских исходных файлов и считать, что импорт {-# SOURCE #-} ссылается на файл hs-boot, граф импорта модулей не должен содержать циклов. Команда ghc -M сообщит об ошибке, если обнаружит цикл.
  • Модуль M, импортируемый в программе через {-# SOURCE #-}, обычно также импортируется обычным способом в другом месте. Если это не так, ghc --make автоматически добавляет M к набору модулей, которые пытается скомпилировать и связать, чтобы реализация M вошла в итоговую программу.

Файл hs-boot должен содержать лишь минимально необходимую информацию для запуска процесса начальной загрузки. Например, ему не нужно содержать объявления всего, что экспортирует модуль A, — достаточно только тех объявлений, которые нужны модулям, рекурсивно импортирующим A.

Файл hs-boot записывается на подмножестве языка Haskell:

  • Заголовок модуля (включая список экспорта) и инструкции импорта записываются точно так же, как в Haskell; правила области видимости также совпадают. Поэтому, чтобы упомянуть тип или класс, отличный от Prelude, его необходимо импортировать.
  • Объявления значений недопустимы, но для значений можно указывать сигнатуры типов. Например:

    double :: Int -> Int
    
  • Объявления фиксации точно такие же, как в Haskell.
  • Обычные объявления синонимов типов точно такие же, как в Haskell.
  • Объявления открытых семейств типов и данных точно такие же, как в Haskell.
  • Закрытое семейство типов можно указать полностью (все уравнения должны совпадать с исходным модулем) или абстрактно, используя синтаксис where .. (то есть опустив уравнения), как в следующем примере:

    type family ClosedFam a where ..
    

    Запись .. следует понимать буквально — в файле нужно написать две точки. Обратите внимание, что предложение where всё равно необходимо, чтобы отличить закрытые семейства от открытых. Если вы указываете уравнения закрытого семейства, необходимо указать их все, в том же порядке, в каком они расположены в соответствующем файле Haskell.

  • Объявление типа данных можно указать полностью, точно как в Haskell, или абстрактно, опустив знак «=» и всё, что за ним следует. Например:

    data T a b
    

    В исходной программе это объявление означало бы, что у TA нет конструкторов (расширение GHC: см. Типы данных без конструкторов), но в файле hi-boot оно означает: «Я не знаю и меня не интересует, какие у него конструкторы». Это наиболее распространённая форма объявления типа данных, поскольку её легко правильно составить. Можно также перечислить конструкторы, но в этом случае объявление должно в точности совпадать с настоящим определением.

    Если конструкторы не перечислены, может потребоваться указать аннотацию вида (Квантификация с явным указанием вида), чтобы сообщить GHC вид переменной типа, если он не равен «*». (В исходных файлах он выводится из способа использования переменной типа в конструкторах.) Например:

    data R (x :: * -> *) y
    

    Нельзя использовать deriving в объявлении типа данных; вместо этого напишите объявление instance.

  • Объявления классов можно указать полностью, точно как в Haskell, или абстрактно, опустив всё, кроме заголовка экземпляра: суперклассы, методы класса и связанные типы. Однако если у класса есть какие-либо ::extension::FunctionalDependencies, указанные в файле hs-boot, они должны совпадать.

    Если объявление класса указано полностью, оно должно быть идентично объявлению в исходном файле, за исключением переименования переменных типа, связанных заголовком класса. Это означает следующее:

    • Заголовок класса должен совпадать.
    • Контекст класса должен совпадать с точностью до упрощения ограничений.
    • Если есть какие-либо ::extension::FunctionalDependencies, они должны совпадать.
    • Порядок, имена и типы методов класса должны совпадать.
    • Арность и виды любых связанных типов должны совпадать.
    • Методы по умолчанию и сигнатуры по умолчанию (см. ::extension::DefaultSignatures) должны быть указаны для одних и тех же методов и совпадать.
    • Объявления значений по умолчанию для связанных типов должны быть указаны для одних и тех же типов и совпадать.

    Чтобы объявить в файле hs-boot класс без методов, у него должен быть суперкласс. Если у класса нет ограничений на суперклассы, добавьте пустое ограничение, например:

    class () => C a
    

    Это полное объявление класса, а не абстрактное объявление с пропущенными методами.

  • Объявления экземпляров можно включать так же, как в Haskell; при этом часть «where» следует опустить.
  • Теперь роль по умолчанию для параметров абстрактного типа данных — representational. (Абстрактный тип данных — это тип, для которого не перечислены конструкторы.) Чтобы задать другую роль, используйте аннотацию роли. (См. Роли.)

5.8.11. Сигнатуры модулей

В GHC 8.2 поддерживаются сигнатуры модулей (файлы hsig), которые позволяют записать сигнатуру вместо реализации модуля, отложив выбор реализации до более позднего момента. Эта возможность не предназначена для использования без Cabal; в этом разделе руководства рассматриваются синтаксис и семантика сигнатур.

Для начала рассмотрим пример. Предположим, у вас есть модуль A, который использует некоторые операции со строками. При обычном импорте модулей можно было бы выбрать только конкретную реализацию строк:

module Str where
    type Str = String

    empty :: Str
    empty = ""

    toString :: Str -> String
    toString s = s

module A where
    import Str
    z = toString empty

Заменив Str.hs сигнатурой Str.hsig, мы параметризуем A (и любые другие модули этого пакета) реализацией строк:

signature Str where
    data Str
    empty :: Str
    toString :: Str -> String

Можно проверить типы A относительно этой сигнатуры или инстанцировать Str модулем, предоставляющим следующие объявления. Более подробное описание инстанцирования сигнатур см. в документации Cabal.

Сигнатуры модулей включают две тесно связанные возможности:

  • Возможность определить файл hsig, содержащий определения типов и сигнатуры типов значений, которые могут использовать модули, импортирующие сигнатуру, и которые должна предоставить будущая реализация;
  • Возможность наследовать обязательные сигнатуры от зависимых пакетов, объединяя их в одну общую сигнатуру, отражающую требования как локально определённой сигнатуры, так и зависимостей.

Файл сигнатуры обозначается расширением hsig; для каждой обязательной сигнатуры должен существовать файл hsig (даже если он пуст), в том числе для обязательных сигнатур, унаследованных от зависимостей. Сигнатуры можно импортировать с помощью обычного объявления import Sig.

Файлы hsig записываются на варианте Haskell, похожем на файлы hs-boot, но с некоторыми небольшими изменениями:

  • Заголовок сигнатуры имеет вид signature A where ... (вместо обычного module A where ...).
  • Инструкции импорта и правила области видимости точно такие же, как в Haskell. Чтобы упомянуть тип или класс, отличный от Prelude, его необходимо импортировать.
  • В отличие от обычных модулей, определённые сущности сигнатуры включают не только сущности, записанные в локальном файле hsig, но и сущности из унаследованных сигнатур (определяемых по флагам -package-id ⟨unit-id⟩). Эти сущности не входят в область видимости при проверке типов локального файла hsig, но доступны для импорта любому модулю или сигнатуре, которые импортируют данную сигнатуру. Единственное исключение из этого правила — список экспорта, описанный ниже.

    Если объявление присутствует в нескольких унаследованных сигнатурах, они объединяются. Для значений типы в обеих сигнатурах должны точно совпадать; другие же объявления могут объединяться более сложным образом. В таких случаях операция объединения текстуально заменяет все вхождения старого имени ссылкой на новое объединённое объявление. Например, если имеются следующие две сигнатуры:

    signature A where
        data T
        f :: T -> T
    
    signature A where
        data T = MkT
        g :: T
    

    итоговая объединённая сигнатура будет выглядеть так:

    signature A where
        data T = MkT
        f :: T -> T
        g :: T
    
  • Если в сигнатуре не указан список экспорта, она экспортирует все свои определённые сущности, объединённые с экспортами всех унаследованных сигнатур.

    Чтобы повторно экспортировать сущность из сигнатуры, необходимо также добавить экспорт module SigName, чтобы экспортировались все сущности, определённые в сигнатуре. Например, следующий модуль экспортирует из Prelude и f, и Int:

    signature A(module A, Int) where
        import Prelude (Int)
        f :: Int
    

    Повторный экспорт объединяется с локальными объявлениями; поэтому приведённая выше сигнатура успешно объединится со следующим объявлением:

    signature A where
        data Int
    

    Единственной допустимой реализацией такой сигнатуры является модуль, который повторно экспортирует в точности ту же сущность:

    module A (f, Int) where
        import Prelude (Int)
        f = 2 :: Int
    

    И наоборот, любую сущность, запрошенную сигнатурой, можно предоставить посредством повторного экспорта из реализующего модуля. Это отличается от файлов hs-boot, которые требуют, чтобы каждая сущность была определена в реализующем модуле локально.

  • GHC экспериментально поддерживает сокращение сигнатур, используемое, когда у сигнатуры есть явный список экспорта без экспорта самой сигнатуры в виде модуля. В этом случае список экспорта применяется к итоговому списку экспорта после объединения. В частности, можно ссылаться на сущности, не объявленные в теле локального файла hsig.

    Семантика в этом случае такова: набор обязательных сущностей определяется исключительно списком экспорта; если сущность не указана в списке экспорта, она не является обязательной. Эта возможность предназначена для того, чтобы автор библиотеки мог предоставить общую сигнатуру с типами всех функций, которые могут понадобиться пользователю, а клиент мог сократить список экспорта до нужных ему сущностей. Например, если вы унаследовали сигнатуру для строк, можно создать локальную сигнатуру такого вида, перечислив только необходимые сущности:

    signature Str (Str, empty, append, concat) where
        -- empty
    

    Здесь есть несколько оговорок. Во-первых, запрещено экспортировать сущность, которая ссылается на локально определённый тип, сам не являющийся экспортируемым (в этом случае GHC сообщит об ошибке). Во-вторых, сигнатуры из зависимостей, экспортирующих модули, нельзя сокращать таким образом (ведь сама зависимость может нуждаться в этой сущности); такие требования экспортируются безусловно. Наконец, любые повторно экспортируемые модули должны быть импортированы локальной сигнатурой, даже если модуль экспортировался унаследованной сигнатурой.

    В будущем синтаксис и семантика этой возможности могут измениться.

  • Объявления и типы из сигнатур зависимостей, которые будут объединены, не входят в область видимости при проверке типов файла hsig. Чтобы обратиться к такому типу, необходимо объявить его самостоятельно:

    -- OK, assuming we inherited an A that defines T
    signature A (T) where
        -- empty
    
    -- Not OK
    signature A (T, f) where
        f :: T -> T
    
    -- OK
    signature A (T, f) where
        data T
        f :: T -> T
    
  • Объявления значений недопустимы, но для значений можно указывать сигнатуры типов. Например, можно определить следующую сигнатуру:

    signature A where
        double :: Int -> Int
    

    Модуль, реализующий A, должен будет экспортировать функцию double с типом, определительно равным типу из сигнатуры. Обратите внимание: это означает, что нельзя реализовать double с помощью полиморфной функции double :: Num a => a -> a.

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

  • Объявления фиксации, синонимов типов и открытых семейств типов/данных разрешены так же, как в обычном Haskell.
  • Объявления закрытых семейств типов разрешены так же, как в обычном Haskell. Их также можно указать абстрактно, как в следующем примере:

    type family ClosedFam a where ..
    

    Запись .. следует понимать буквально — в файле нужно написать две точки. Предложение where отличает закрытые семейства от открытых.

  • Объявление типа данных можно указать полностью, точно как в Haskell, или абстрактно, опустив знак «=» и всё, что за ним следует. Например:

    signature A where
        data T a b
    

    Абстрактные типы данных можно реализовать не только объявлениями data, но и newtype или синонимами типов (при условии, что синоним типа полностью редуцирован по eta, например type T = ...). Например, все следующие варианты являются допустимыми реализациями приведённого выше T:

    -- Algebraic data type
    data T a b = MkT a b
    
    -- Newtype
    newtype T a b = MkT (a, b)
    
    -- Type synonym
    data T2 a b = MkT2 a a b b
    type T = T2
    

    Объявления типов данных объединяются только с другими объявлениями типов данных, которые в точности им соответствуют; исключение — абстрактные типы данных, которые можно объединить с объявлениями data, newtype или type. Объединение с синонимами типов особенно полезно. Предположим, вы используете пакет строк, в котором тип символов строки не задан:

    signature Str where
        data Str
        data Elem
        head :: Str -> Elem
    

    Если локально определить сигнатуру, задающую type Elem = Char, то теперь можно использовать head из унаследованной сигнатуры так, как если бы она возвращала Char.

    Если конструкторы не перечислены, может понадобиться указать вид, чтобы сообщить GHC виды переменных типа, если они не равны значению по умолчанию *. В отличие от обычных объявлений типов данных, возвращаемый вид абстрактного объявления типа данных может быть любым (в таком случае, вероятно, оно будет реализовано с помощью синонима типа). Это можно использовать для полиморфизма представления на этапе компиляции (в отличие от полиморфизма представления во время выполнения), как в следующем примере:

    signature Number where
        import GHC.Types
        data Rep :: RuntimeRep
        data Number :: TYPE Rep
        plus :: Number -> Number -> Number
    

    Роли параметров типа подчиняются отношению подтипизации phantom < representational < nominal: например, абстрактный тип с номинальным параметром типа можно реализовать конкретным типом с репрезентативным параметром типа. При объединении учитывается это отношение подтипизации (например, nominal в сочетании с representational даёт representational). Для ролей в сигнатурах по умолчанию используется nominal, что обеспечивает максимальную гибкость для реализующей стороны. Явную аннотацию роли нужно указывать только в том случае, если клиент сигнатуры хочет выполнять приведение абстрактного типа в параметре типа (в этом случае следует явно указать representational). В отличие от обычных типов данных, мы не предполагаем, что абстрактные типы данных инъективны по представлению: если задано Coercible (T a) (T b), а для T установлена роль nominal, это не означает, что a ~ b.

  • Объявления классов могут быть абстрактными или конкретными. Абстрактный класс — это класс без суперклассов и методов класса:

    signature A where
        class Key k
    

    Его можно реализовать любым способом, с любым набором суперклассов и методов; однако модулям, зависящим от абстрактного класса, запрещено определять экземпляры (в GHC 8.2 это ограничение не проверяется; см. #13086). Эти объявления можно реализовать синонимами типов вида Constraint; это может быть полезно, если требуется параметризовать функцию ограничением. Например, при включённом расширении ConstraintKinds следующий синоним типа является допустимой реализацией приведённой выше сигнатуры:

    module A where
        type Key = Eq
    

    Конкретный класс задаёт свои суперклассы, методы, сигнатуры методов по умолчанию (но не их реализации) и прагму MINIMAL. В отличие от обычных классов Haskell, для того чтобы метод был необязательным в отношении прагмы MINIMAL, не нужно явно объявлять для него реализацию по умолчанию.

    При объединении объявлений классов суперклассы и методы должны совпадать в точности; однако прагмы MINIMAL объединяются логической операцией ИЛИ, а метод с сигнатурой по умолчанию успешно объединяется с методом без такой сигнатуры.

  • Объявления экземпляров можно включать так же, как в Haskell; достаточно опустить часть «where». Объявление экземпляра не обязательно должно быть реализовано напрямую: если экземпляр можно вывести на основе экземпляров из окружения, он считается реализованным. Например, следующая сигнатура:

    signature A where
        data Str
        instance Eq Str
    

    считается реализованной следующим модулем, поскольку имеются экземпляры Eq для [] и Char, которые можно объединить в экземпляр Eq [Char]:

    module A where
        type Str = [Char]
    

    В отличие от других объявлений, для которых в область видимости входят только сущности, объявленные в файле сигнатуры, экземпляры из реализации всегда входят в область видимости, даже если они не были объявлены в файле сигнатуры. Это означает, что модуль может пройти проверку типов относительно сигнатуры, но не пройти её относительно соответствующей реализации. Избежать таких ситуаций можно, не определяя осиротевшие экземпляры внутри пакета с сигнатурами.

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

  • Любые осиротевшие экземпляры, попавшие в область видимости вследствие импорта сигнатуры, безусловно считаются находящимися в области видимости, даже если итоговый реализующий модуль не импортирует те же осиротевшие экземпляры.

Известные ограничения:

  • Синонимы шаблонов не поддерживаются.
  • Алгебраические типы данных, указанные в сигнатуре, нельзя реализовать с помощью синонимов шаблонов. См. #12717

5.8.12. Использование make

Настроить Makefile для работы с GHC довольно просто, если имена исходных файлов совпадают с именами модулей. Например:

HC      = ghc
HC_OPTS = -cpp $(EXTRA_HC_OPTS)

SRCS = Main.lhs Foo.lhs Bar.lhs
OBJS = Main.o   Foo.o   Bar.o

.SUFFIXES : .o .hs .hi .lhs .hc .s

cool_pgm : $(OBJS)
        rm -f $@
        $(HC) -o $@ $(HC_OPTS) $(OBJS)

# Standard suffix rules
.o.hi:
        @:

.lhs.o:
        $(HC) -c $< $(HC_OPTS)

.hs.o:
        $(HC) -c $< $(HC_OPTS)

.o-boot.hi-boot:
        @:

.lhs-boot.o-boot:
        $(HC) -c $< $(HC_OPTS)

.hs-boot.o-boot:
        $(HC) -c $< $(HC_OPTS)

# Inter-module dependencies
Foo.o Foo.hc Foo.s    : Baz.hi          # Foo imports Baz
Main.o Main.hc Main.s : Foo.hi Baz.hi   # Main imports Foo and Baz

Примечание

Более сложные варианты make позволяют элегантнее реализовать некоторые из описанных выше решений. В частности, правила шаблонов gmake позволяют записать более понятный вариант:

%.o : %.lhs
        $(HC) -c $< $(HC_OPTS)

Приведённый вариант должен работать с любым make.

Обратите внимание на довольно простое правило .o.hi: оно фиксирует зависимость файла интерфейса (.hi) от исходного файла. Согласно правилу, файл .hi можно создать из файла .o, ничего не делая. Что соответствует действительности.

Обратите внимание, что правила суффиксов повторяются дважды: один раз для обычных исходных файлов Haskell, второй — для файлов hs-boot (см. Взаимно рекурсивные модули и файлы hs-boot).

Обратите также внимание на межмодульные зависимости в конце Makefile, имеющие следующий вид:

Foo.o Foo.hc Foo.s    : Baz.hi          # Foo imports Baz

Они сообщают make, что если дата изменения любого из файлов Foo.o, Foo.hc или Foo.s предшествует дате изменения Baz.hi, то устаревший файл необходимо обновить. Чтобы обновить его, make ищет подходящее правило; одно из приведённых выше правил суффиксов отлично справляется с этой задачей. Эти зависимости можно автоматически сгенерировать с помощью ghc; см. Генерация зависимостей

5.8.13. Генерация зависимостей

Вручную добавлять в Makefile межмодульные зависимости вида Foo.o : Bar.hi довольно рискованно. Не беспокойтесь: GHC поддерживает автоматическую генерацию необходимых зависимостей. Добавьте в Makefile следующее:

depend :
        ghc -M $(HC_OPTS) $(SRCS)

Теперь перед началом компиляции, а также при каждом изменении imports в программе, выполните make depend перед make cool_pgm. Команда ghc -M добавит необходимые зависимости в Makefile.

В общем случае ghc -M Foo выполняет следующие действия. Для каждого модуля M из набора Foo и всех его импортов (транзитивно) она добавляет в Makefile:

  • Строку, фиксирующую зависимость объектного файла от исходного файла.

    M.o : M.hs
    

    (или M.lhs, если вы использовали такое имя файла).

  • Для каждого объявления импорта import X в M — строку, фиксирующую зависимость M от X:

    M.o : X.hi
    
  • Для каждого объявления импорта import {-# SOURCE #-} X в M — строку, фиксирующую зависимость M от X:

    M.o : X.hi-boot
    

    (Подробности о файлах интерфейса в стиле hi-boot см. в разделе Взаимно рекурсивные модули и файлы hs-boot.)

Если M импортирует несколько модулей, будет создано несколько строк с M.o в качестве цели.

Не нужно перечислять все исходные файлы в аргументах команды ghc -M; ghc отслеживает зависимости так же, как ghc --make (новая возможность в GHC 6.4).

Обратите внимание, что ghc -M должна найти исходный файл для каждого модуля в графе зависимостей, чтобы разобрать объявления импорта и проследить зависимости. Поэтому любые предварительно скомпилированные модули без исходных файлов должны принадлежать пакету[1].

По умолчанию ghc -M генерирует все зависимости и добавляет их в конец файла makefile (или Makefile, если makefile не существует), заключая их между строками «# DO NOT DELETE: Beginning of Haskell dependencies» и «# DO NOT DELETE: End of Haskell dependencies». Если эти строки уже есть в makefile, старые зависимости сначала удаляются.

Не забудьте использовать те же параметры -package в командной строке ghc -M, что и при компиляции; это позволит генератору зависимостей находить импортированные модули из пакетов. Однако модули пакетов не будут включены в сгенерированные зависимости (см. параметр -include-pkg-deps ниже).

Этап генерации зависимостей в GHC допускает несколько дополнительных параметров, которые могут быть полезны. Параметры, влияющие на генерацию зависимостей:

-ddump-mod-cycles

Вывести список циклов в графе модулей. Это полезно при попытке устранить такие циклы.

-v2

Вывести полный список зависимостей модулей в стандартный поток вывода. (Это стандартный флаг подробного вывода, поэтому список также будет отображаться с -v3 и -v4; см. раздел Параметры подробного вывода.)

-dep-makefile ⟨file⟩

Использовать ⟨file⟩ в качестве makefile вместо makefile или Makefile. Если ⟨file⟩ не существует, mkdependHS создаст его. Часто мы используем -dep-makefile .depend, чтобы поместить зависимости в .depend, а затем include файл .depend в Makefile.

-dep-suffix ⟨suffix⟩

Создавать зависимости, указывающие, что файлы с суффиксом .⟨suf⟩⟨osuf⟩ зависят от файлов интерфейса с суффиксом .⟨suf⟩hi или (для импортов {-# SOURCE #-}) от .hi-boot. Допускается несколько флагов -dep-suffix. Например, -dep-suffix a_ -dep-suffix b_ создаст зависимости .hs от .hi, .a_hs от .a_hi и .b_hs от .b_hi. Если этот флаг не использовать, применяется пустой суффикс.

-exclude-module=⟨file⟩

Считать ⟨file⟩ «стабильным», то есть исключить его из списка файлов, от которых зависят другие файлы.

-include-pkg-deps

Считать импортированные из пакетов модули нестабильными, то есть генерировать зависимости от любых импортированных модулей пакетов (включая Prelude и все остальные стандартные библиотеки Haskell). Рекурсивное отслеживание зависимостей внутри пакетов не выполняется; зависимости генерируются только для модулей основного пакета, непосредственно импортирующих модули внешних пакетов. Этот параметр обычно используется только различными системными библиотеками.

-include-cpp-deps

Выводить зависимости препроцессора. Этот параметр действует только при включённом языковом расширении CPP. К таким зависимостям относятся файлы, включённые директивой препроцессора #include (а также транзитивные включения), и неявно включаемые файлы, например стандартные заголовки препроцессора C и заголовок с версией GHC. Одно исключение: при компиляции GHC создаёт временный заголовочный файл с макросами версий пакетов. Поскольку это временный файл, который GHC всегда создаёт самостоятельно, он не выводится как зависимость.

5.8.14. Модули-сироты и объявления экземпляров

В Haskell указано, что при компиляции модуля M видны все объявления экземпляров во всех модулях «ниже» M. (Модуль A находится «ниже» M, если A импортирован непосредственно модулем M или если A находится ниже модуля, который напрямую импортирует M.) Следовательно, в принципе GHC должен считывать файлы интерфейсов всех модулей ниже M на случай, если они содержат объявление экземпляра, имеющее значение для M. На практике это было бы катастрофой, поэтому GHC старается действовать более избирательно.

В частности, если объявление экземпляра находится в том же модуле, что и определение любого типа или класса, упомянутого в заголовке объявления экземпляра (части после «=>»; см. Правила завершения экземпляров), GHC в любом случае должен обратиться к этому файлу интерфейса. Например:

module A where
  instance C a => D (T a) where ...
  data T a = ...

Объявление экземпляра имеет значение, только если используется тип T, и в этом случае GHC уже обратился бы к файлу интерфейса A, чтобы найти определение T.

Проблема возникает только тогда, когда модуль содержит объявление экземпляра и у GHC нет других причин обращаться к этому модулю. Например:

module Orphan where
  instance C a => D (T a) where ...
  class C a where ...

Здесь ни D, ни T не объявлены в модуле Orphan. Такие модули называются «модулями-сиротами». GHC определяет модули-сироты и обращается к файлу интерфейса каждого модуля-сироты ниже компилируемого модуля. Обычно это напрасная работа, но избежать её нельзя. Поэтому следует стараться свести количество модулей-сирот к минимуму.

Функциональные зависимости усложняют ситуацию. Предположим, у нас есть:

module B where
  instance E T Int where ...
  data T = ...

Является ли это модулем-сиротой? На первый взгляд, нет, поскольку T объявлен в том же модуле. Но предположим, что у класса E есть функциональная зависимость:

module Lib where
  class E x y | y -> x where ...

Тогда в некотором импортирующем модуле M ограничение (E a Int) следует «улучшить», задав a = T, даже если в M нет явного упоминания T.

Эти соображения приводят к следующему определению модуля-сироты:

  • Модуль-сирота — это модуль, содержащий как минимум один экземпляр-сироту или как минимум одно правило-сироту.
  • Объявление экземпляра в модуле M является экземпляром-сиротой, если

    • Класс объявления экземпляра не объявлен в M и
    • либо у класса нет функциональных зависимостей и ни один из конструкторов типов в заголовке экземпляра не объявлен в M; либо существует функциональная зависимость, для которой ни один из конструкторов типов, упомянутых в неопределяемой части заголовка экземпляра, не определён в M.

    Учитывается только заголовок экземпляра. В приведённом выше примере недостаточно, чтобы объявление C находилось в модуле A; это должно быть объявление D или T.

  • Правило переписывания в модуле M является правилом-сиротой, если ни одна из переменных, конструкторов типов или классов, свободных в левой части правила, не объявлена в M.

Если использовать флаг -Worphans, GHC предупредит о создании модуля-сироты. Как и любое предупреждение, его можно отключить с помощью -Wno-orphans, а флаг -Werror приведёт к сбою компиляции, если будет выдано предупреждение.

Определить модуль-сироту можно, просмотрев его файл интерфейса M.hi с помощью --show-iface ⟨file⟩ в режиме. Если в первой строке есть [orphan module], GHC считает этот модуль модулем-сиротой.

[1]

Это изменение поведения по сравнению с версией 6.2 и более ранними версиями.

© 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.14.1/docs/users_guide/separate_compilation.html

Spec-Zone.ru

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