Haskell/Modules
Модули — основное средство организации кода на Haskell. Мы мимоходом встречали их при использовании операторов import для помещения библиотечных функций в нашу область видимости. Помимо улучшения использования библиотек, знание модулей поможет нам придавать форму нашим программам и создавать самостоятельные программы, которые могут выполняться независимо от GHCi (кстати, это тема следующей главы «Самостоятельные программы»).
Модули
правитьМодули Haskell[1] — удобный способ группировки связанной функциональности в один пакет и управления различными функциями, которые могут иметь одинаковые имена. Определение модуля находится в самом начале вашего Haskell файла.
Простейшее определение модуля выглядит так:
module YourModule where
Обратите внимание, что
- имя модуля начинается с заглавной буквы;
- каждый файл содержит только один модуль.
Имя файла — это имя модуля плюс файловое расширение .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 не будет использоваться.
Внимание! Существует неоднозначность между уточненным именем вроде MyModule.remove_e и оператором композиции функций (.) . Написание reverse.MyModule.remove_e собьет с толку компилятор Haskell. Одно из решений -- стилистическое: всегда использовать пробелы для композиции функций, например reverse . remove_e или Just . remove_e или даже Just . MyModule.remove_e
|
Сокрытие определений
правитьТеперь предположим, мы хотим импортировать оба вышеуказанных модуля, но уверены, что будем удалять буквы 'е' обоих регистров. Было бы утомительно добавлять 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(..)), указывая, что экспортируются все конструкторы.
Содержание в порядке списков экспорта — это хорошая практика не только из-за уменьшения загрязнения пространства имён, но и из-за того, что списки экспорта дают возможность для применения определённых оптимизаций времени компиляции, без них недоступных.
- ↑ Для дополнительной информации о системе модулей смотрите Haskell report.
- ↑ В Haskell98, последней стандартизированной версии Haskell перед Haskell 2010, система модулей весьма консервативна, но последняя общая практика состоит в использовании иерархической системы модулей, используя точки для разделения пространств имён.
- ↑ Модуль может экспортировать импортированные функции. Взаимно-рекурсивные модули возможны, но требуют специального обращения.