Опубликован: 19.09.2008 | Уровень: специалист | Доступ: платный
Лекция 6:

Модули

< Лекция 5 || Лекция 6: 123 || Лекция 7 >

5.4. Импортирование и экспортирование объявлений экземпляров

Объявления экземпляров нельзя явно указать в списках импорта или экспорта. Все экземпляры в области видимости модуля всегда экспортируются, и любое объявление импорта вводит в область видимости все экземпляры импортируемого модуля. Таким образом, объявление экземпляра находится в области видимости, если и только если цепочка объявлений import ведет к модулю, содержащему объявление экземпляра.

Например, import M() не вводит никакие новые имена из модуля M в область видимости, но вводит все экземпляры, которые видны в M. Модуль, чья единственная цель состоит в том, чтобы обеспечить объявления экземпляров, может иметь пустой список экспорта. Например,

module MyInstances() where
    instance Show (a -> b) where
show fn = "<<function>>"
    instance Show (IO a) where
show io = "<<IO action>>"

5.5. Конфликт имен и замыкание

5.5.1. Квалифицированные имена

Квалифицированное имя имеет вид modid.name (идентификатор-модуля.имя) (раздел "2.4" ). Квалифицированное имя вводится в область видимости:

  • Посредством объявления верхнего уровня. Объявление верхнего уровня вводит в область видимости и неквалифицированное, и квалифицированное имя определяемой сущности. Так:
    module M where
        f x = ...
        g x = M.f x x

    является правильным объявлением. Определяемое вхождение должно ссылаться на неквалифицированное имя; поэтому будет неправильным писать

    module M where
        M.f x = ... - НЕПРАВИЛЬНО
        g x = let M.y = x+1 in ... - НЕПРАВИЛЬНО
  • Посредством объявления import. Объявление import, с инструкцией qualified или без, всегда вводит в область видимости квалифицированное имя импортированной сущности (раздел "5.3" ). Это позволяет заменить объявление импорта с инструкцией qualified на объявление без инструкции qualified без изменений ссылок на импортированные имена.

5.5.2. Конфликты имен

Если модуль содержит связанное вхождение имени, например, f или A.f, должна быть возможность однозначно решить, на какую сущность при этом ссылаются; то есть должно быть только одно связывание для f или A.f соответственно.

Ошибки не будет, если существуют имена, которые нельзя так разрешить, при условии, что программа не содержит ссылок на эти имена. Например:

module A where
    import B
    import C
    tup = (b, c, d, x)
  
  module B( d, b, x, y ) where
    import D
    x = ...
    y = ...
    b = ...
  
  module C( d, c, x, y ) where
    import D
    x = ...
    y = ...
    c = ...

  module D( d ) where
    d = ...

Рассмотрим определение tup.

  • Ссылки на b и c можно однозначно разрешить: здесь подразумевается соответственно b, объявленный в B, и c, объявленный в C.
  • Ссылка на d однозначно разрешается: здесь подразумевается d, объявленный в D. В этом случае та же сущность вводится в область видимости двумя путями (импорт B и импорт C ), и на нее можно ссылаться в A посредством имен d, B.d и C.d.
  • Ссылка на x является неоднозначной: она может означать x, объявленный в B, или x, объявленный в C. Неоднозначность может быть разрешена путем замены x на B.x или C.x.
  • Нет ни одной ссылки на y, поэтому нет ошибки в том, что различные сущности с именем y экспортируют и B, и C. Сообщение об ошибке появится только в том случае, если будет ссылка на y.

Имя, встречающееся в сигнатуре типа или infix-объявлениях, всегда является неквалифицированным и однозначно ссылается на другое объявление в том же списке объявлений (за исключением того, что infix-объявление для метода класса может встречаться на верхнем уровне, см. раздел "4.4.2" ). Например, следующий модуль является правильным:

module F where

    sin :: Float -> Float
    sin x = (x::Float)

    f x = Prelude.sin (F.sin x)

Локальное объявление sin является правильным, даже если sin из Prelude неявно находится в области видимости. Ссылки на Prelude.sin и F.sin должны быть обе квалифицированными для того, чтобы однозначно определить, какой подразумевается sin. Тем не менее, неквалифицированное имя sin в сигнатуре типа в первой строке F однозначно ссылается на локальное объявление sin.

5.5.3. Замыкание

Каждый модуль в программе на Haskell должен быть замкнутым. То есть каждое имя, явно указанное в исходном тексте, должно быть локально определено или импортировано из другого модуля. Тем не менее, нет необходимости в том, чтобы сущности, которые требуются компилятору для контроля типов или другого анализа времени компиляции, были импортированы, если к ним нет обращений по имени. Система компиляции Haskell несет ответственность за нахождение любой информации, необходимой для компиляции без помощи программиста. То есть импорт переменной x не требует, чтобы типы данных и классы в сигнатуре x были введены в модуль наряду с x, если к этим сущностям не обращаются по имени в пользовательской программе. Система Haskell молча импортирует любую информацию, которая должна сопровождать сущность для контроля типов или любых других целей. Такие сущности не требуется даже явно экспортировать: следующая программа является правильной, хотя T не избегает M1:

module M1(x) where
    data T = T
    x = T
  
  module M2 where
    import M1(x)
    y = x

В этом примере нет способа указать явную сигнатуру типа для y, т.к. T не находится в области видимости. Независимо от того, экспортируется T явно или нет, модуль M2 знает достаточно о T, чтобы правильно выполнить контроль соответствия типов программы.

На тип экспортируемой сущности не влияет неэкспортируемые синонимы типов. Например, в

module M(x) where
    type T = Int
    x :: T
    x = 1

типом x является и T, и Int ; они взаимозаменяемы, даже когда T не находится в области видимости. То есть определение T доступно любому модулю, который сталкивается с ним, независимо от того, находится имя T в области видимости или нет. Единственная причина экспортировать T состоит в том, чтобы позволить другим модулям обращаться к нему по имени; контроль типов находит определение T, если оно необходимо, независимо от того, было оно экспортировано или нет.

5.6. Стандартное начало (Prelude)

Многие возможности Haskell определены в самом Haskell в виде библиотеки стандартных типов данных, классов и функций, называемой "стандартным началом (prelude)." В Haskell стандартное начало содержится в модуле Prelude. Есть также много предопределенных модулей библиотеки, которые обеспечивают менее часто используемые функции и типы. Например, комплексные числа, массивы, и большинство операций ввода - вывода являются частью стандартной библиотеки. Они описаны в части II. Отделение библиотеки от Prelude имеет преимущество в виде сокращения размера и сложности Prelude, позволяя ему более легко становиться частью программы и расширяя пространство полезных имен, доступных программисту.

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

5.6.1. Модуль Prelude

Модуль Prelude автоматически импортируется во все модули, как если бы была инструкция 'import Prelude', если и только если он не импортируется посредством явного объявления import. Это условие для явного импорта позволяет выборочно импортировать сущности, определенные в Prelude, точно так же, как сущности из любого другого модуля.

Семантика сущностей в Prelude задана ссылочной реализацией Prelude, написанной на Haskell , данной в лекции "8" . Некоторые типы данных (например, Int) и функции (например, сложение Int) нельзя задать непосредственно на Haskell . Так как обработка таких сущностей зависит от реализации, они формально не описаны в лекции "8" . Реализация Prelude также является неполной при обработке кортежей: должно быть бесконечное семейство кортежей и объявлений их экземпляров, но реализация лишь задает схему.

В лекции "8" дано определение модуля Prelude с использованием нескольких других модулей: PreludeList, PreludeIO и так далее. Эти модули не являются частью Haskell 98, и их нельзя импортировать отдельно. Они просто помогают объяснить структуру модуля Prelude ; их следует рассматривать как часть ее реализации, а не часть определения языка.

5.6.2. Сокрытие имен из Prelude

Правила о Prelude были разработаны так, чтобы имелась возможность использовать имена из Prelude для нестандартных целей; тем не менее, каждый модуль, который так делает, должен иметь объявление import, которое делает это нестандартное использование явным. Например:

module A( null, nonNull ) where
    import Prelude hiding( null ) 
    null, nonNull :: Int -> Bool
    null    x = x == 0
    nonNull x = not (null x)

Модуль A переопределяет null и содержит неквалифицированную ссылку на null в правой части nonNull. Последнее было бы неоднозначно без наличия инструкции hiding(null) в объявлении import Prelude. Каждый модуль, который импортирует неквалифицированное имя A и затем создает неквалифицированную ссылку на null, должен также разрешить неоднозначное использование null так же, как это делает A. Таким образом, есть небольшая опасность случайно скрыть имена из Prelude.

Имеется возможность создать и использовать другой модуль, который будет служить вместо Prelude. За исключением того факта, что модуль Prelude неявно импортируется в любой модуль, Prelude является обычным модулем Haskell ; он является особенным только в том, что обращение к некоторым сущностям Prelude происходит посредством специальных синтаксических конструкций. Переопределение имен, используемых Prelude, не влияет на значение этих специальных конструкций. Например, в

module B where
    import Prelude()
    import MyPrelude
    f x = (x,x)
    g x = (,) x x
    h x = [x] ++ []

явное объявление import Prelude() предотвращает автоматический импорт Prelude, в то время как объявление import MyPrelude вводит в область видимости нестандартное начало (prelude). Специальный синтаксис для кортежей (например, (x,x) и (,)) и списков (например, [x] и [] ) продолжает обращаться к кортежам и спискам, определенным стандартным Prelude; не существует способа переопределить значение [x], например, в терминах другой реализации списков. С другой стороны, использование ++ не является специальным синтаксисом, поэтому он обращается к ++, импортированному из MyPrelude.

Невозможно, тем не менее, скрыть объявления instance в Prelude. Например, нельзя определить новый экземпляр для Show Char.

5.7. Раздельная компиляция

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

5.8. Абстрактные типы данных

Способность экспортировать тип данных без его конструкторов позволяет конструировать абстрактные типы данных (ADT). Например, ADT для стеков можно определить так:

module Stack( StkType, push, pop, empty ) where
    data StkType a = EmptyStk | Stk a (StkType a)
    push x s = Stk x s
    pop (Stk _ s) = s
    empty = EmptyStk

Модули, импортирующие Stack, не могут создавать значения типа StkType, потому что они не имеют доступа к конструкторам типа. Вместо этого они должны использовать push, pop и empty, чтобы создать такие значения.

Также имеется возможность строить ADT на верхнем уровне существующего типа посредством использования объявления newtype. Например, стеки можно определить через списки:

module Stack( StkType, push, pop, empty ) where
    newtype StkType a = Stk [a]
    push x (Stk s) = Stk (x:s)
    pop (Stk (_:s)) = Stk s
    empty = Stk []
< Лекция 5 || Лекция 6: 123 || Лекция 7 >
KroshkaRu KroshkaRu
KroshkaRu KroshkaRu
Россия, Петерубрг, СПБ-ГПУ, 1998
Петр Бондареко
Петр Бондареко
Россия