5.2Модули
Haskell поддерживает модульное программирование, то есть программа может быть разделена на модули, и каждый модуль может быть использован в нескольких программах.
Каждый модуль должен иметь вид:
[module <Имя> where]
<код>
Если необязательная часть опущена, то данный модуль не сможет быть использован из других модулей. Имя модуля, также как и типы данных, должно начинаться с прописной буквы, а также совпадать с именем файла.
Для получения доступа к определениям (функции, типы данных, классы) во внешних модулях, необходимо в самом начале программы описать их следующим образом:
[module <Имя> where]
import <модуль1>
...
import <модульN>
<остальной код>
После этого все определения из этих модулей становятся доступными в текущем модуле. Но если в текущем модуле определены функции или типы данных, совпадающие по имени с импортируемыми, либо если в разных подключаемых модулях окажутся одноименные функции или типы данных, то возникнет ошибка. Чтобы этого избежать, необходимо запретить использование таких определений из соответствующих модулей. Это делается следующим образом:
import <модуль> hiding (<скрываемые определения>)
Кроме того, при описании самого импортируемого модуля есть возможность управлять доступом к определениям. Для этого нужно перечислить доступные определения в скобках после имени модуля:
module <Имя>(<определения>) where <код>
Из всех описаний для внешних модулей будут доступны только перечисленные в скобках.
В качестве примера можно привести программу из 3х модулей:
--Prog.hsmodule Prog where
import Mod1
import Mod2 hiding(modfun)
--Mod1.hs
module Mod1(modfun) where
modfun = five * 2
five = 5
--Mod2.hs
module Mod2 where
modfun = 20
Из модуля Prog доступна функция modfun, определенная в модуле Mod1, но не доступна функция five.
6Классы и монады
6.1Классы
В Haskell'е поддерживается объектно-ориентированная парадигма. Но она немного отличается от привычной, принятой в других языках. Экземпляром класса является структура данных. Каждый класс предполагает описание некоторых функций для работы с типами данных, которые являются экземплярами этого класса.
Для определения класса используется следующая запись:
сlass [(<ограничения>) =>] <имя> <переменная типов> where <функции>
Переменная типов именует некий тип, который должен быть экземпляром этого класса. После слова where распологаются описания типов функций, а также выражение некоторых из этих функций через друг друга. С помощью этих выражений интерпретатор сможет сам определить часть функций экземпляра класса, если задана другая часть. Ограничения – это определение того, что экземпляр нашего класса также должен быть экземпляром перечисленных здесь классов. Это, своего рода, наследование. Рассмотрим пример:
class Eq a where
(==), (/=) :: a -> a -> Bool
x == y = not (x/=y)
x /= y = not (x==y)
Класс Eq является классом сравнимых типов. Для каждого экземпляра этого класса должны быть определены функции равенства и неравенства. Вторая строка означает, что функции (==) и (/=) должны принимать два аргумента типа a и возвращать объект типа Bool, то есть True или False.
Следующие строчки в определении класса говорят о том, что если определена функция (/=), то функция (==) определяется через нее соответствующим образом, и наоборот. Благодаря этому программисту достаточно определить только функцию сравнения на равенство (или неравенство), а другую функцию интерпретатор определит сам.
Еще пример определения класса, но с наследованием:
class Eq a => MyClass a where
myFunc :: [a] -> a -> Int
Когда класс определен, можно объявить какой-либо тип данных экземпляром этого класса:
instance MyClass Double where
myFunc [] _ = 0
myFunc (x:xs) z
|x==z = 1 + myFunc xs z
|otherwise = myFunc xs z
Таким образом мы объявляем стандартный тип Double экземпляром нашего класса MyClass и определяем функцию myFunc как функцию, вычисляющую количество элементов в первом аргументе, равных второму аргументу функции.
Определив тип как экземпляр класса, можно применять к объектам этого типа описанные при этом функции.
test = myFunc x 2 where
x :: [Double]
x = [1,2,3,2,1]
Классы часто используются при описании функций с полиморфными параметрами. Например, если нужно описать функцию, использующую операцию сравнения, для многих типов данных, необходимо указать, что ее параметры, которые участвуют в сравнении, должны быть типа класса Eq, который гарантирует реализацию соответствующей функции.
Когда программист описывает свою структуру данных, она не принадлежит ни к какому из классов. При необходимости программист должен реализовать соответствующие функции для своей структуры и указать ее принадлежность к классу. Например, ранее описанную структуру данных Tree можно объявить экземпляром класса Show, для того что бы интерпретатор мог выводить ее на экран, не прибегая к ручному вызову функции showtree. Для этого напишем:
instance Show a => Show (Tree a) where
show = showtree
После этого при получении интерпретатором результата типа Tree он сможет вывести его на экран, а также любая другая функция сможет перевести объект типа Tree в строковый вид с помощью функции show.
Кроме того, существует еще один способ объявления принадлежности типа к классу. Он более прост, но не всегда может удовлетворить потребности программиста. Он заключается в перечислении нужных классов сразу при объявлении структуры:
data <имя> = <значение1> | <значение2> | ... | <значениеN> deriving (<класс1>,<класс2>,...,<классM>)
Здесь необходимые функции будут при возможности выведены автоматически. Так, например, определим наш тип Tree экземплярам класса Eq, для того чтобы деревья можно было сравнивать между собой.
data Tree a = Nil | Tree a (Tree a) (Tree a)
deriving (Eq)
Теперь возможно сравнение деревьев операцией “==”, и позволяет использовать их в функциях, требующих это.
6.2Ввод-вывод
Возможно написать побольше про монадыКак было сказано выше, Haskell — чистый функциональный язык программирования, то есть функции в нем не могут иметь побочных эффектов, и порядок вычисления функций не определен. Но в некоторых случаях без этого обойтись нельзя. К таким случаям можно отнести работу с пользователем, с файлами, с базами данных и так далее. В языке Haskell предусмотрено задание таких функций с помощью монад — особых контейнерных классов. В изучении Haskell'а монады считается самой трудной частью, поэтому начнем сразу с примеров. Объяснение некоторых вещей будут опущены, чтобы не запутать читателя. Рассмотрим следующий пример:
main = do
putStrLn "Hello world!"
putStrLn "Good bye world!"
В этом примере функция main обладает побочными эффектами — в результате ее вычисления на экране отображается текст, и в ней определен порядок вычисления — сначала на экран выводится первая строка — затем вторая.
При использовании нотации do программирование «нечистых» функций с помощью монад сводится практически к привычному императивному программированию.
Каждая следующая строчка должна быть функцией специального типа — монадического типа. В случае работы с вводом-выводом это тип (IO a).
Для работы с командной строкой используются следующие функции:
putChar :: Char -> IO () -- вывод символа
putStr :: String -> IO () -- вывод строки
putStrLn :: String -> IO () -- вывод строки с переходом на новую строку
getChar :: IO Char -- прочитать символ
getLine :: IO String -- прочитать строку
IO a — это монадический тип ввода-вывода, скрывающий побочные эффекты в себе. Тип IO String означает, что функция вернет результат, содержащий строку, а IO () означает, что функция возвращает результат, ничего не содержащий, смысл ее вызова только в побочных эффектах (например, вывод). Пример:
main = do
putStrLn "What is you name?"
name <- getLine
putStrLn ("Hi, " ++ name ++ "!")
Здесь появилось «присваивание». По аналогии с императивными языками можно сказать, что в строке
name <- getLine
произошло присваивание переменной name результата функции getLine. Но, как мы знаем, в Haskell'е нет переменных и значит и присваиваний. В данном случае произошло создание некоторого объекта с именем name, значение которого равно результату вычисления функции getLine. То есть, если после этого написать еще раз
name <- getLine
то создастся новый объект, имя которого перекроет предыдущий.
Таким образом выполняется извлечение результатов монадических функций. Для того чтобы задать подобным образом «переменные» значениями обыкновенных функций используется упрощенная нотация let:
let name = "John"
Ветвление вычислительного процесса осуществляется с помощью тех же if then else и case:
main = do
putStrLn "What is you name?"
name <- getLine
case name of
"GHC" -> putStrLn "No! I am GHC!"
_ -> putStrLn ("Hi, " ++ name ++ "!")
Если ветвление должно состоять из нескольких функций, то используется ключевое слово do:
main = do
putStrLn "What is you name?"
name <- getLine
case name of
"GHC" -> do
putStrLn "No! I am GHC!"
main
_ -> putStrLn ("Hi, " ++ name ++ "!")
Циклы осуществляются с помощью рекурсии, как показано выше, или с помощью специальных функций (реализованных также рекурсивно). К таким функциям относится функция mapM_. Принцип ее работы аналогичен функции map для списков — она применяет монадическую функцию ко всем элементам списка и последовательно выполняет их. Пример:
writeDigit x = do
putStr " "
putStr (show x)
main = do
putStr "Digits:"
mapM_ writeDigit [0..9]
putStrLn ""
7Примеры
Привести различные примеры (деревья поиска, AVL дерево, еще что-нибудь) Заключение
Заключение Список использованных источников