data Tree a = Leaf a | Branch (Tree a) (Tree a)
leafList (Leaf x) = [x]
leafList (Branch left right) = leafList left ++ leafList right
Модуль явно экспортирует Tree, Leaf, BranchиleafList. Экспортируемые из модуля имена перечисляются в скобках после ключевого слова module. Если это перечисление не указано, по умолчанию из модуля экспортируются все имена. Заметьте, что имена типа и его конструкторов должны быть сгруппированы, как в конструкции Tree(Leaf,Branch). В качестве сокращения можно использовать запись Tree(..). Также возможно экспортировать только часть конструкторов данных.
Модуль Tree теперь может быть импортирован в какой-либо другой модуль:
Module Main where import Tree (Tree(Leaf,Branch), leafList)
Здесь мы явно указали список импортируемых сущностей; если опустить его, импортируются все сущности, экспортируемые из модуля.
Очевидно, если в двух импортируемых модулях содержатся различные сущности с одним именем, возникнет проблема. Для того, чтобы избежать ее в языке существует ключевое слово qualified, при помощи которого определяются те импортируемые модули, имена объектов которых приобретают вид: «Модуль.Объект». Например, для модуля Tree:
Module Main where import qualified Tree
leafList = Tree.leafList
4.8. Абстрактные типы данных
Использование модулей позволяет определять абстрактные типы данных, т. е. типы, внутренняя структура которых скрыта от их пользователя. Например, рассмотрим простейший словарь, содержащий список слов и их значений:
Module Dictionary where
data Dictionary = Dictionary [(String,String)]
getMeaning :: Dictionary -> String -> Maybe String
getMeaning [] _ = Nothing
getMeaning ((word,meaning):xs) w | w == word = Just meaning
| otherwise = getMeaning xs w
Функция getMeaning по заданному словарю и слову возвращает найденное значение (с использованием типа Maybe). Сам словарь представляется списком пар.
Пользователь этого модуля может определить addWord, которая добавляет пару «слово-значение» в словарь и возвращает модифицированный словарь:
Import Dictionary
addWord (Dictionary dict) word meaning = Dictionary ((word,meaning):dict)
Здесь пользователь видит представление словаря в виде списка и может воспользоваться этим. Однако в дальнейшем мы можем захотеть изменить представление словаря. Список — довольно неэффективная структура данных для поиска, если он становится велик. Гораздо лучше использовать хеш-таблицы или деревья поиска. Однако, если представление типа Dictionary открыто, мы не можем изменить его без риска нарушить функционирование пользовательских программ.
Сделаем тип Dictionary абстрактным, чтобы скрыть от пользователей модуля его внутреннее представление. Определим в модуле значение emptyDict, представляющее собой пустой словарь и функцию addWord. Тогда пользователи смогут общаться со значениями типа Dictionary только с помощью разрешенных функций:
Module Dictionary (Dictionary, getMeaning, addWord, emptyDict) where
data Dictionary = Dictionary [(String,String)]
getMeaning :: Dictionary -> String -> Maybe String
getMeaning [] _ = Nothing
getMeaning ((word,meaning):xs) w | w == word = Just meaning
| otherwise = getMeaning xs w
addWord (Dictionary dict) w m = Dictionary ((w,m):dict)
emptyDict = Dictionary []
Абстрактные типы данных предоставляют механизм сокрытия данных, который в объектно-ориентированных языках программирования называется инкапсуляцией.
4.9. Операции ввода-вывода
4.9.1. Базовые операции ввода-вывода
В отличие от других функций операции ввода-вывода выполняют некоторое действие и возвращают некоторое значение. В системе типов это значение «помечено» типом IO, который отличает действия от других значений. Например, рассмотрим функцию getChar:
GetChar :: IO Char
IO Char показывает, что getChar при вызове выполняет некоторое действие, которое возвращает символ. Действия, которые не возвращают результата, используют тип IO (). Символ () означает пустой тип. Например, функция putChar:
putChar :: Char -> IO ()
Она принимает символ и не возвращает ничего.
Действия связываются друг с другом с помощью оператора >>=, который определяется посредством монад (см. далее), может быть объяснен с помощью do-нотации. Ключевое слово do начинает последовательность операторов, которые выполняются по порядку. Оператор может быть либо действием, либо образцом, связываемым с результатом действия с помощью <- (присваивание).Следующая программа считывает символ и печатает его:
Main :: IO ()
main = do c <-getChar
PutChar c
Функция main модуля Main является точкой входа в программу на языке HASKELL. Ее тип должен быть IO ().
Допустим, нам необходимо определить функцию ready, которая считывает символ и возвращает True, если он равен ’y’. Нам нужно создать действие, которое ничего не делает, но возвращает это некоторое значение в качестве результата. Для этого служит функция return:
return :: a -> IO a
Таким образом, ready определяется так:
Ready :: IO Bool
ready = do c <-getChar
return (c == ’y’)
Теперь можно определить более сложные функции ввода-вывода. Функция getLine, возвращающая строку, считанную с клавиатуры с символом конца строки в качестве завершающего:
GetLine :: IO String
getLine = do c <-getChar
if c== ’\n’
then return ""
else do l <-getLine
Return (c:l)
Действие ввода-вывода не может быть выполнено в обычном выражении, поскольку оно не оперирует типом IO.
Дата: 2016-10-02, просмотров: 262.