Модули — основное средство организации кода на Haskell. Мы мимоходом встречали их при использовании операторов import для помещения библиотечных функций в нашу область видимости. Помимо улучшения использования библиотек, знание модулей поможет нам придавать форму нашим программам и создавать самостоятельные программы, которые могут выполняться независимо от GHCi (кстати, это тема следующей главы «Самостоятельные программы»).

МодулиПравить

Модули Haskell[1] — удобный способ группировки связанной функциональности в один пакет и управления различными функциями, которые могут иметь одинаковые имена. Определение модуля находится в самом начале вашего Haskell файла.

Простейшее определение модуля выглядит так:

module YourModule where

Обратите внимание, что

  1. имя модуля начинается с заглавной буквы;
  2. каждый файл содержит только один модуль.

Имя файла — это имя модуля плюс файловое расширение .hs; любые точки в имени модуля означают каталоги.[2] Так что модуль YourModule будет в файле YourModule.hs, а модуль Foo.Bar будет в файле Foo/Bar.hs или Foo\Bar.hs. Имена файлов, как и имена модулей, начинаются с заглавных букв.

ИмпортПравить

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

import Data.Char (toLower, toUpper) -- импортировать только функции toLower и toUpper из Data.Char

import Data.List -- импорт всего экспортированного из Data.List
 
import MyModule -- импорт всего экспортированного из MyModule

Импортированные типы данных определяются как имя типа и следующие за ним конструкторы в скобках, например:

import Data.Tree (Tree(Node)) -- импорт только типа Tree и его конструктора Node из Data.Tree

Что если импорт некоторых модулей имеет пересекающиеся определения? Или если вы импортируете модуль, но хотите переписать какую-то имеющуюся в нем функцию самостоятельно? Есть три способа контроля: уточненный импорт, сокрытие определений и переименовывание импортированного содержимого.

Уточненный (квалифицированный) импортПравить

Пусть два модуля MyModule и MyOtherModule имеют по определению remove_e удаляющего все экземпляры e из строки, при этом версия из MyModule удаляет только буквы нижнего регистра, а версия из MyOtherModule удаляет как буквы верхнего, так и нижнего регистров. В этом случае следующий код неоднозначен:

import MyModule
import MyOtherModule

-- someFunction добавляет 'c' в начало и удаляет все 'e' из строки
someFunction :: String -> String
someFunction text = 'c' : remove_e text

Неясно, какая из функций remove_e имеется в виду! Для избежания подобной ситуации используется ключевое слово qualified:

import qualified MyModule
import qualified MyOtherModule

someFunction text = 'c' : MyModule.remove_e text -- правильно, удаляет 'e' нижнего регистра
someOtherFunction text = 'c' : MyOtherModule.remove_e text -- правильно, удаляет 'e' обоих регистров
someIllegalFunction text = 'c' : remove_e text -- неправильно, remove_e не определена

В последнем фрагменте кода функция remove_e вообще отсутствует. Когда делается уточненный импорт, все импортированные значения имеют префикс — имя модуля. Кстати, можно использовать те же префиксы даже если импорт был обыкновенным (не уточненным). В нашем примере MyModule.remove_e сработает даже если ключевое слово qualified не будет использоваться.


Сокрытие определенийПравить

Теперь предположим, мы хотим импортировать оба вышеуказанных модуля, но уверены, что будем удалять буквы 'е' обоих регистров. Было бы утомительно добавлять MyOtherModule. перед каждым вызовом remove_e. Нельзя ли просто из импорта MyModule исключить remove_e?

import MyModule hiding (remove_e)
import MyOtherModule

someFunction text = 'c' : remove_e text

Этот код работает благодаря слову hiding в строке импорта. Всё следущее за ключевым словом «hiding» исключается из импорта. Для сокрытия нескольких элементов перечислите их в скобках через запятую:

import MyModule hiding (remove_e, remove_f)

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

Переименовывание импортаПравить

Это в действительности не техника для переопределения (overwriting), но она часто используется при уточнённом импорте. Представьте такой код:

import qualified MyModuleWithAVeryLongModuleName

someFunction text = 'c' : MyModuleWithAVeryLongModuleName.remove_e $ text

Это раздражает, особенно если используется уточненный импорт. Можно улучшить положение используя ключевое слово as:

import qualified MyModuleWithAVeryLongModuleName as Shorty

someFunction text = 'c' : Shorty.remove_e $ text

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

Пока нет конфликтов, можно импортировать несколько модулей под одно и то же имя:

import MyModule as My
import MyCompletelyDifferentModule as My

В этом случае и функции из MyModule, и функции из MyCompletelyDifferentModule могут использовать префикс My.

Совмещение переименовывание с ограниченным импортомПравить

Иногда удобно делать импорт одного модуля дважды. Типичный сценарий такой:

import qualified Data.Set as Set
import Data.Set (Set, empty, insert)

Это даёт доступ ко всему модулю Data.Set через псевдоним Set, а также дает доступ к конструктору Set, и функциям empty и insert напрямую, без префикса Set.

ЭкспортПравить

В примере в начале этой статьи использовались слова «импорт всего экспортированного из Data.List».[3] Встаёт вопрос: как мы можем решить, какие функции экспортируются, а какие остаются «внутренними»? Ответ таков:

module MyModule (remove_e, add_two) where

add_one blah = blah + 1

remove_e text = filter (/= 'e') text

add_two blah = add_one . add_one $ blah

Здесь экспортируются только remove_e и add_two. Хотя add_two может использовать add_one, функции в модуле, импортирующем MyModule не могут использовать add_one напрямую, она не экспортирована.

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

module MyModule2 (Tree(Branch, Leaf)) where

data Tree a = Branch {left, right :: Tree a} 
            | Leaf a

Тут объявление модуля может быть переписано как MyModule2 (Tree(..)), указывая, что экспортируются все конструкторы.

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

  1. Для дополнительной информации о системе модулей смотрите Haskell report.
  2. В Haskell98, последней стандартизированной версии Haskell перед Haskell 2010, система модулей весьма консервативна, но последняя общая практика состоит в использовании иерархической системы модулей, используя точки для разделения пространств имён.
  3. Модуль может экспортировать импортированные функции. Взаимно-рекурсивные модули возможны, но требуют специального обращения.