Spec-Zone.ru › Haskell 8

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
Подразумевает

-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 покажет вам, какие временные файлы были сгенерированы.

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

-tmpdir ⟨dir⟩

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

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

7.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⟩ — имя файла интерфейса, выводит содержимое этого интерфейса в удобочитаемом формате. См. Режимы работы.

7.8.8. Опции, связанные с расширенными файлами интерфейса

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

  • упрощённое 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
    

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

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

  • Вы можете включить объявления экземпляров как в 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

Spec-Zone.ru

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