Основы функционального программирования/Haskell/Модули и монады: различия между версиями
Содержимое удалено Содержимое добавлено
Ramir (обсуждение | вклад) Нет описания правки |
|||
Строка 1:
__TOC__▼
{{ОФП Содержание}}
▲__TOC__
В [[w:Haskell|Хаскеле]] также есть понятие модуля. Но более завораживающее
== Модули ==
В Хаскеле модули несут двоякое назначение: с одной стороны, они необходимы для контроля за пространством имён (как
Определить модуль в Хаскеле достаточно просто.
Как известно, на верхнем уровне модуля в Хаскеле может быть множество деклараций (описаний и определений): типы, классы, данные, функции. Однако один вид деклараций должен стоять в модуле на первом месте (если этот вид деклараций вообще используется). Речь идет о включении в модуль других модулей
Определение модуля должно начинаться со служебного слова <code>module</code>. Например, ниже приведено определение модуля <code>Tree</code>:
<code>module Tree (Tree (Leaf, Branch), fringe) where
data Tree a = Leaf a
Строка 23:
fringe :: Tree a -> [a]
fringe (Leaf x) = [x]
fringe (Branch left right) = fringe left ++ fringe right</code>
В этом модуле описан один тип:
Использование модуля в другом модуле выглядит еще проще:
<code>module Main where
import Tree (Tree(Leaf, Branch), fringe)
main = print (fringe (Branch (Leaf 1) (Leaf 2)))</code>
В приведенном примере видно, что модуль <code>Main</code> импортирует модуль <code>Tree</code>, причём в декларации <code>import</code> явно описано, какие именно объекты импортируются из модуля Tree. Если это описание опустить, то импортироваться будут все объекты, которые модуль экспортирует, то есть в данном случае можно было просто написать: <code>import Tree</code>.
<code>module Main where
import qualified Tree
main = print (Tree.fringe (Tree.Leaf ’a’))</code>
Использование такого синтаксиса полностью лежит на совести программиста. Некоторым нравится полная определённость, которую привносят квалифицированные имена, и они используют их в ущерб размеру программ. Другим нравится использовать короткие мнемонические имена, и они используют квалификаторы (имена модулей) только по необходимости.
=== Абстрактные типы
В Хаскеле модуль является единственным способом создать так называемые абстрактные типы данных, то есть такие, в которых скрыто представление типа, но открыты только специфические операции над созданным типом, набор которых вполне достаточен для работы с типом. Например, хотя тип <code>Tree</code> является достаточно простым, его все-таки лучше сделать абстрактным типом, то есть скрыть то, что <code>Tree</code> состоит из <code>Leaf</code> и <code>Branch</code>. Это делается следующим образом:
<code>module TreeADT (Tree, leaf, branch, cell, left, right, isLeaf) where
data Tree a = Leaf a
Строка 62:
right (Branch l r) = r
isLeaf (Leaf _) = True
isLeaf _ = False</code>
Видно, что к внутренностям типа Tree внешний пользователь (программист) может пробраться только используя определённые функции. Впоследствии, когда создатель этого модуля захочет изменить представление типа (например, оптимизировать его), ему необходимо будет изменить и функции, которые оперируют полями типа Tree. В свою очередь, программист, использовавший в своей программе тип Tree, ничего менять не будет, ибо его программа все так же останется работоспособной.
Строка 70:
Далее приводятся дополнительные аспекты системы модулей в Хаскеле:
*В декларации импорта (<code>import</code>) можно выборочно спрятать некоторые из экспортируемых объектов служебным словом <code>hiding</code>. Это бывает полезным для явного исключения определений некоторых объектов из импортируемого модуля.
*При импорте можно определить псевдоним модуля для квалификации имен экспортируемых из него объектов. Для этого используется служебное слово <code>as</code>. Это может быть полезным для того укорачивания имен модулей.
*Все программы неявно импортируют модуль <code>Prelude</code>. Если сделать явный импорт этого модуля, то в его декларации возможно скрыть некоторые объекты, чтобы впоследствии их переопределить.
*Все декларации <code>instance</code> неявно экспортируются и импортируются всеми модулями.
*Методы классов могут быть так же как и подтипы данных перечислены в скобках после имени соответствующего класса во время декларации экспорта/импорта.
Строка 80:
Многие новички в функциональном програмировании бывают часто озадачены понятием монады в Хаскеле. Но монады очень часто встречаются в языке: так, например, система ввода/вывода основана именно на понятии монады, а стандартные библиотеки содержат целые модули, посвящённые монадам. Необходимо отметить, что понятие монады в Хаскеле основано на [[w:теория категорий|теории категорий]], но чтобы не вдаваться в абстрактную математику, далее будет представлено интуитивное понимание монад.
Монады суть типы, представляющие собой экземпляры одного из следующих монадических классов: <code>Functor</code>, <code>Monad</code> и <code>MonadPlus</code>. Ни один из этих классов не может быть предком для другого класса, то есть монадические классы ненаследуемы. В модуле <code>Prelude</code> определены три монады: <code>IO</code>, <code>[]</code> и <code>Maybe</code>, то есть список также является монадой.
Математически монада определяется через набор правил, которые связывают операции, производимые над монадой. Эти правила дают интуитивное понимание того, как должна использоваться монада и какова ее внутренняя структура. Для конкретизации далее рассматривается класс Monad, в котором определены две базовые операции и одна функция:
<code>class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
Строка 90:
fail :: String -> m a
m >> k = m >>= \_ -> k</code>
Две операции <code>(>>=)</code> и <code>(>>)</code> — это операции связывания. Они комбинируют два монадических значения, тогда как функция return преобразует переданное значение некоторого типа a в монадическое значения типа <code>m a</code>. Сигнатура операции (<code>>></code>) помогает понять операцию связывания: выражение
Точное значение операция связывания конечно же зависит от конкретной реализации монады. Так, например, тип <code>IO</code> определяет операцию <code>(>>=)</code> как последовательное выполнение двух её операндов, а результат выполнения первого операнда последовательно передается во второй. Для двух других встроенных монадических типов (списки и <code>Maybe</code>) эта операция определена как передача нуля или более параметров из одного вычислительного процесса в следующий.
В Хаскеле определено служебное слово, на уровне языка поддерживающее использование монад: слово <code>do</code>, понимание которого можно увидеть в следующих правилах его применения:
<code>do e1 ; e2 = e1 >> e2
do p <- e1 ; e2 = e1 >>= \p -> e2</code>
Первое выражение выполняется всегда (переноса значений из первого операнда во второй не производится). Во втором выражении может произойти ошибка, в этом случае производится вызов функции <code>fail</code>, которая также определена в классе <code>Monad</code>. Поэтому более точное определение служебного слова <code>do</code> для второго случая будет выглядеть так:
<code>do p <- e1 ; e2 = e1 >>= (\v -> case v of
p -> e2
_ -> fail ”s”)</code>
где s — это строка, которая может определять местоположение оператора <code>do</code> в программе, а может просто нести некоторую семантическую нагрузку. Например, в монаде <code>IO</code> действие <code>(‘a’ getChar)</code> вызовет функцию <code>fail</code> в том случае, если считанный символ не является символом <code>‘a’</code>. Это действие прерывает исполнение программы, т.к. определенная в монаде <code>IO</code> функция <code>fail</code> в свою очередь вызывает системную функию <code>error</code>.
Класс <code>MonadPlus</code> используется для тех монад, в которых есть нулевой элемент и операция «<code>+</code>». Определение этого класса выглядит следующим образом:
<code>class (Monad m) => MonadPlus m where
mzero :: m a
mplus :: m a -> m a -> m a</code>
Нулевой элемент в этом классе монад подчиняется следующим правилам:
<code>m >>= \x -> mzero = mzero
mzero >>= m = mzero</code>
Например, для списков нулевым элементом является пустой список <code>[]</code>, а операцией «<code>+</code>» — конкатенация списков. Поэтому монада списков является экземпляром класса <code>MonadPlus</code>. С другой стороны монада <code>IO</code> не имеет нулевого элемента, поэтому
=== Встроенные монады ===
Строка 125:
Для конкретизации полученных сведений необходимо рассмотреть что-нибудь более приземленное. Так как списки являются монадами и в то же время списки достаточно скрупулезно изучены, они являются отличным материалом, на котором можно рассмотреть практическое применение механизма монад.
Для списков операция связывания обретает смысл в соединении вместе набора операций, производимых над каждым элементом списка. При использовании со списками сигнатура операции <code>(>>=)</code>
<code>(>>=) :: [a] -> (a -> [b]) -> [b]</code>
Это обозначает, что дан список значений типа <code>a</code> и функция, которая проецирует значение типа <code>a</code> на список значений типа <code>b</code>. Связывание применяет функцию к каждому элементу данного списка значений типа <code>a</code> и возвращает полученный список значений типа <code>b</code>. Эта операция уже известна
'''Выражение 1'''
<code>[(x, y) | x <- [1, 2, 3], y <- [1, 2, 3], x /= y]</code>
'''Выражение 2'''
<code>do x <- [1, 2, 3]
y <- [1, 2, 3]
True <- return (x /= y)
return (x, y)</code>
'''Выражение 3'''
<code>[1, 2, 3] >>= (\x -> [1, 2, 3] >>= (\y -> return (x /= y) >>=
(\r -> case r of
True -> return (x, y)
_ -> fail ””)))</code>
Какое выражение использовать в написании программ — выбирать программисту.
Строка 150 ⟶ 151 :
== Упражнения ==
== Ответы для самопроверки ==
|