Перейти к содержанию

Haskell/Modules

Материал из Викиучебника — открытых книг для открытого мира

Модули — основное средство организации кода на 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. Модуль может экспортировать импортированные функции. Взаимно-рекурсивные модули возможны, но требуют специального обращения.