7.8. Имена файлов и отдельная компиляция
В этом разделе описано, какие файлы ожидает найти GHC, какие файлы он создаёт, где хранятся эти файлы и какие опции влияют на это поведение.
Конвенции именования путей различаются в зависимости от системы. В частности, разделитель каталогов — «/» в системах Unix и «\» в системах Windows. В следующих разделах мы будем последовательно использовать «/» в качестве разделителя каталогов; замените его соответствующим символом для вашей системы.
7.8.1. Файлы исходного кода Haskell
Каждый модуль Haskell исходного кода должен располагаться в отдельном файле.
Обычно файл должен быть назван по имени модуля, заменяя точки в имени модуля разделителями каталогов. Например, в системе Unix модуль A.B.C должен располагаться в файле A/B/C.hs, относительно некоторого базового каталога. Если модуль не будет импортироваться другим модулем (например, Main), то вы можете использовать любое имя файла для него.
GHC предполагает, что файлы исходного кода имеют кодировку ASCII или UTF-8, другие кодировки не распознаются. Однако некорректные последовательности UTF-8 будут проигнорированы в комментариях, поэтому можно использовать другие кодировки, такие как Latin-1, при условии, что исходный код, за исключением комментариев, использует только ASCII.
7.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⟩ — имя модуля с точками, заменёнными на слэши. GHC будет молча создавать необходимую структуру каталогов в каталоге ⟨dir⟩, если она ещё не существует.
Имя файла интерфейса выводится по тем же правилам, за исключением того, что суффикс — ⟨hisuf⟩ (.hi по умолчанию) вместо ⟨osuf⟩, и соответствующие опции — -hidir ⟨dir⟩ и -hisuf ⟨suffix⟩ вместо -odir ⟨dir⟩ и -osuf ⟨suffix⟩ соответственно.
Например, если GHC компилирует модуль A.B.C в файле src/A/B/C.hs, без флагов -odir или -hidir, файл интерфейса будет помещён в src/A/B/C.hi, а файл объекта — в src/A/B/C.o.
Для любого импортируемого модуля GHC требует, чтобы имя модуля в операторе импорта точно соответствовало имени модуля в файле интерфейса (или файле исходного кода), найденном с помощью стратегии, описанной в Пути поиска. Это означает, что для большинства модулей имя файла исходного кода должно совпадать с именем модуля.
Однако обратите внимание, что разумно иметь модуль Main в файле с именем foo.hs, но это работает только потому, что GHC никогда не нуждается в поиске интерфейса для модуля Main (потому что он никогда не импортируется). Поэтому можно иметь несколько модулей Main в отдельных файлах исходного кода в одном каталоге, и GHC не запутается.
В режиме пакетной компиляции имя файла объекта также можно переопределить с помощью опции -o ⟨file⟩, а имя файла интерфейса можно указать непосредственно с помощью опции -ohi ⟨file⟩.
7.8.3. Путь поиска
В вашей программе вы импортируете модуль Foo следующим образом: import Foo. В режиме --make или GHCi GHC будет искать файл исходного кода для Foo и позаботится о его компиляции в первую очередь. Без режима --make, GHC будет искать файл интерфейса для Foo, который должен был быть создан в ходе предыдущей компиляции Foo. GHC использует ту же стратегию в каждом из этих случаев для поиска соответствующего файла.
Эта стратегия такова: GHC хранит список каталогов, называемый путём поиска. Для каждого из этих каталогов он пытается добавить ⟨basename⟩.⟨extension⟩ к каталогу и проверяет, существует ли файл. Значение ⟨basename⟩ — имя модуля с точками, заменёнными разделителем каталогов («/» или «\\", в зависимости от системы), а ⟨extension⟩ — расширение файла исходного кода (hs, lhs) если мы находимся в режиме --make или GHCi, или ⟨hisuf⟩ в противном случае.
Например, предположим, что путь поиска содержит каталоги d1, d2, и d3, и мы находимся в режиме --make и ищем файл исходного кода для модуля A.B.C. GHC будет искать его в d1/A/B/C.hs, d1/A/B/C.lhs, d2/A/B/C.hs и так далее.
По умолчанию путь поиска содержит один каталог: «.» (т. е. текущий каталог). Следующие опции могут быть использованы для добавления в путь поиска или изменения его содержимого:
-
-i⟨dir⟩[:⟨dir⟩]* -
Этот флаг добавляет список каталогов, разделённых двоеточием, в путь поиска.
-
-i -
сбрасывает путь поиска до пустого значения.
Это не все: GHC также ищет модули в предварительно скомпилированных библиотеках, известных как пакеты. Подробности см. в разделе о пакетах (Пакеты).
7.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).
-
-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⟩ -
Вывод интерфейса может быть направлен в другой файл
bar2/Wurble.ifaceс помощью опции-ohi bar2/Wurble.iface(не рекомендуется).Предупреждение
Если вы перенаправите файл интерфейса в место, где GHC его не найдёт, тогда проверка повторной компиляции может сбиться с толку (по крайней мере, вы не получите никаких преимуществ от избегания повторной компиляции). Мы рекомендуем использовать сочетание опций
-hidirи-hisufвместо этого, если это возможно.Чтобы вообще не генерировать интерфейс, вы можете использовать эту опцию для перенаправления интерфейса в корзину:
-ohi /dev/null, например.
-
-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"(см. Использование внешнего экспорта и внешнего импорта ccall "обёртки" с GHC). Опция-stubdirведет себя точно так же, как-odirи-hidirотносительно иерархических модулей.
-
-dumpdir ⟨dir⟩ -
Перенаправляет все файлы вывода в ⟨dir⟩. Файлы вывода генерируются, когда
-ddump-to-fileиспользуется с другими флагами-ddump-*.
-
-outputdir ⟨dir⟩ -
Опция
-outputdirявляется сокращением для комбинации-odir ⟨dir⟩,-hidir ⟨dir⟩,-stubdir ⟨dir⟩и-dumpdir ⟨dir⟩.
-
-osuf ⟨suffix⟩ -
Суффикс
-osuf⟨suffix⟩ изменит суффикс файла.oдля файлов объектов на заданный вами. Мы используем это при компиляции библиотек, чтобы объекты для профилирующих версий библиотек не перезаписывали обычные.
-
-hisuf ⟨suffix⟩ -
Аналогично,
-hisuf⟨suffix⟩ изменит суффикс файла.hiдля файлов интерфейса, не относящихся к системе (см. Другие опции, связанные с файлами интерфейса).Игра
-hisuf/-osufособенно полезна, если вы хотите скомпилировать программу как с профилированием, так и без него, в одном каталоге. Вы можете сказать:ghc ...
чтобы получить обычную версию, и
ghc ... -osuf prof.o -hisuf prof.hi -prof -fprof-auto
чтобы получить профилируемую версию.
-
-hiesuf ⟨suffix⟩ -
Опция
-hiesuf⟨suffix⟩ изменит суффикс файла.hieдля расширенных файлов интерфейса на заданный вами.
-
-hcsuf ⟨suffix⟩ -
Наконец, опция
-hcsuf⟨suffix⟩ изменит суффикс файла.hcдля промежуточных файлов C, сгенерированных компилятором.
7.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 -
- Подразумевает
Сохранение промежуточных файлов
.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покажет вам, какие временные файлы были сгенерированы.
7.8.6. Перенаправление временных файлов
-
-tmpdir ⟨dir⟩ -
Если у вас проблемы из-за нехватки места в
/tmp(или где ваша установка предполагает хранение временных файлов), вы можете использовать опцию-tmpdir ⟨dir⟩для указания альтернативного каталога. Например,-tmpdir .говорит о размещении временных файлов в текущей рабочей директории.В качестве альтернативы, используйте переменную среды
TMPDIR. Установите её в имя каталога, в котором должны храниться временные файлы. GCC и другие программы также будут учитывать переменнуюTMPDIR.
7.8.7. Другие опции, связанные с файлами интерфейса
-
-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⟩ — имя файла интерфейса, выводит содержимое этого интерфейса в удобочитаемом формате. См. Режимы работы.
7.8.8. Опции, связанные с расширенными файлами интерфейса
-
упрощённое AST
- узлы аннотированы позициями в исходном коде и типами
- идентификаторы аннотированы информацией о области видимости
- исходный код Haskell в сыром виде
API GHC предоставляет функции для чтения и записи этих файлов.
-
-fwrite-ide-info -
Записывает расширенные файлы интерфейса вместе с обычными файлами интерфейса. Как и обычные файлы интерфейса, GHC имеет проверку повторной компиляции для обнаружения устаревших или отсутствующих расширенных файлов интерфейса.
-
-fvalidate-ide-info -
Выполняет ряд проверок целостности и проверок стиля на расширенных файлах интерфейса, которые записываются. Они включают в себя проверку таких свойств, как переменные, не встречающиеся за пределами их ожидаемых областей видимости.
Формат, в котором GHC в настоящее время хранит свой типизированное АСТ, делает сбор типов для некоторых узлов выражений дорогостоящим. Для повышения производительности GHC в настоящее время пропускает их, поэтому не ожидается, что все узлы выражений будут иметь информацию о типах. См. #16233 для получения дополнительной информации.
7.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) каждого файла интерфейса и каждой декларации в файле интерфейса. Он также сохраняет в каждом файле интерфейса список отпечатков всего, что он использовал при последней компиляции файла. Если дата изменения исходного файла раньше даты файла .o (то есть исходный файл не изменился с момента последней компиляции), и проверка повторной компиляции включена, GHC будет действовать умно. Он сравнивает отпечатки необходимых в этот раз элементов с отпечатками элементов, которые ему потребовались в прошлый раз (полученные из файла интерфейса компилируемого модуля); если они все одинаковы, он останавливает компиляцию на ранней стадии, заявив «Перекомпиляция НЕ требуется». Какая красота!
Вы можете узнать как все это работает в документации GHC.
7.8.10. Как компилировать взаимно рекурсивные модули
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-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. Если вы укажете каталог для файлов интерфейсов с помощью флага-ohidir, то это также повлияет на файлы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.
-
Закрытое семейство типов может необязательно опустить свои уравнения, как в следующем примере:
type family ClosedFam a where ..
..подразумевается буквально — в вашем файле нужно написать две точки. Обратите внимание, чтоwhere-оператор всё ещё необходим, чтобы отличить закрытые семейства от открытых. Если вы укажете какие-либо уравнения для закрытого семейства, вы должны указать все из них в том же порядке, что и в сопровождающем файле Haskell. -
Декларация типа данных может быть указана полностью, как в Haskell, или абстрактно, опуская знак «=» и всё, что следует за ним. Например:
data T a b
В исходной программе это объявляло бы TA без конструкторов (расширение GHC: см. Типы без конструкторов), но в файле hi-boot это означает «Я не знаю и не забочусь о том, каковы конструкторы». Это самая распространённая форма декларации типа данных, так как её легко правильно записать. Вы можете *также* записать конструкторы, но, если вы это сделаете, вы должны записать их точно так же, как и в их реальном определении.
Если вы не записываете конструкторы, вам может потребоваться указать аннотацию типа (Явно типизированные кванторы), чтобы сообщить GHC о типе переменной типа, если это не «*». (В файлах исходного кода это вычисляется из того, как переменная типа используется в конструкторах.) Например:
data R (x :: * -> *) y
Вы не можете использовать
derivingв декларации типа данных; запишите вместо этого декларациюinstance. - Декларации классов такие же, как в Haskell, за исключением того, что вы не можете указывать декларации методов по умолчанию. Вы также можете полностью опустить все суперклассы и методы класса; но вы должны либо опустить их все, либо указать все.
- Вы можете включать декларации экземпляров так же, как и в Haskell; но опустите часть «where».
- По умолчанию роль параметров абстрактного типа теперь представляет собой роль. (Абстрактный тип — это тип без указанных конструкторов.) Чтобы получить другую роль, используйте аннотацию роли. (См. Роли.)
7.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экспорт, чтобы все сущности, определённые в подписи, были экспортированы. Например, следующий модуль экспортирует какf, так иIntизPrelude: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Абстрактные типы данных могут быть реализованы не только с объявлениями данных, но и с 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Конкретный класс определяет свои суперклассы, методы, сигнатуры методов по умолчанию (но не их реализации) и псевдоним
MINIMALpragma. В отличие от обычных классов Haskell, вам не нужно явно объявлять значение по умолчанию для метода, чтобы сделать его необязательным относительноMINIMALpragma.При объединении объявлений класса мы требуем, чтобы суперклассы и методы совпадали точно; однако
MINIMALpragmas логически объединяются по ИЛИ, и метод с сигнатурой по умолчанию успешно объединится с методом без неё. -
Вы можете включить объявления экземпляров как в Haskell; просто опустите часть «where». Объявление экземпляра не обязательно должно быть реализовано напрямую; если экземпляр может быть выведен на основе экземпляров в окружении, он считается реализованным. Например, следующая подпись:
signature A where data Str instance Eq Strсчитается реализованной следующим модулем, так как существуют экземпляры
Eqдля[]иChar, которые могут быть объединены для образования экземпляраEq [Char]:module A where type Str = [Char]В отличие от других объявлений, для которых в область видимости попадают только сущности, объявленные в файле подписи, экземпляры из реализации всегда попадают в область видимости, даже если они не были объявлены в файле подписи. Это означает, что модуль может пройти проверку типов против подписи, но не против соответствующей реализации. Вы можете избежать таких ситуаций, если никогда не будете определять сторонние экземпляры внутри пакета с подписями.
Объявления экземпляров объединяются только если их заголовки точно совпадают, поэтому может возникнуть ситуация, когда GHC посчитает экземпляры в подписи перекрывающимися, даже если они реализованы непересекающимся способом. Если у вас возникнут проблемы, обратитесь к нам.
- Любые сторонние экземпляры, которые попадают в область видимости с помощью импорта из подписи, безусловно, считаются в области видимости, даже если конечный реализующий модуль фактически не импортирует те же сторонние экземпляры.
Известные ограничения:
- Синтаксис шаблонов не поддерживается.
- Алгебраические типы данных, указанные в подписи, не могут быть реализованы с помощью синтаксиса шаблонов. См. #12717
7.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 (см. Как компилировать взаимно рекурсивные модули).
Обратите также внимание на межмодульные зависимости в конце Makefile, которые имеют вид
Foo.o Foo.hc Foo.s : Baz.hi # Foo imports Baz
Они сообщают make, что если у любого из Foo.o, Foo.hc или Foo.s дата последней модификации раньше, чем у Baz.hi, то устаревший файл необходимо обновить. Для обновления make ищет правило для этого; одно из предыдущих правил суффикса успешно выполняет эту задачу. Эти зависимости могут быть сгенерированы автоматически ghc; см. Генерация зависимостей
7.8.13. Генерация зависимостей
Вручную вносить межзависимости вида Foo.o : Bar.hi в ваш Makefile довольно подвержено ошибкам. Не волнуйтесь, GHC поддерживает автоматическую генерацию необходимых зависимостей. Добавьте следующее в ваш Makefile:
depend :
ghc -dep-suffix '' -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.)
Если 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⟩ в качестве файла make, а не
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. Обратите внимание, что вы должны указать по крайней мере один суффикс; если вы не хотите суффикс, укажите-dep-suffix ''.
-
--exclude-module=⟨file⟩ -
Рассматривать
⟨file⟩как «стабильный»; т.е., исключить его из зависимостей от него.
-
-include-pkg-deps -
Рассматривать модули, импортированные из пакетов, как нестабильные, т.е., генерировать зависимости от любых импортированных модулей пакетов (включая
Prelude, и все другие стандартные библиотеки Haskell). Зависимости не отслеживаются рекурсивно в пакеты; зависимости генерируются только для модулей домашнего пакета от модулей внешнего пакета, непосредственно импортируемых модулем домашнего пакета. Эта опция обычно используется только различными системными библиотеками.
-
-include-cpp-deps -
Выводить зависимости препроцессора. Это имеет эффект только при включении расширения языка CPP. Эти зависимости являются файлами, включаемыми с помощью директивы препроцессора
#include(а также включаемые транзитивно) и неявными включениями, такими как стандартные заголовки препроцессора C и заголовок версии GHC. Одно исключение из этого заключается в том, что GHC генерирует временный заголовочный файл (во время компиляции), содержащий макросы версии пакета. Поскольку это только временный файл, который GHC всегда будет генерировать, он не выводится как зависимость.
7.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.
Эти соображения приводят к следующему определению модуля-сироты:
- Модуль-«сирота» orphan module содержит по крайней мере один сирота-экземпляр или по крайней мере одно правило-«сирота».
-
Декларация экземпляра в модуле
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/8.10.2/docs/html/users_guide/separate_compilation.html