Основы функционального программирования/Haskell/Модули и монады: различия между версиями

Содержимое удалено Содержимое добавлено
Нет описания правки
Строка 1:
__TOC__
{{ОФП Содержание}}
__TOC__
 
Ни один языкЯзыки программирования не обходитсяобходятся без понятия [[w:модуль (программирование)|модуля]], разве что толькоза языкиисключением языков самого [[w:Низкоуровневый язык программирования|низкого уровня]], да и те в последнее время приобретают свойствавысокоуровневые языков более высокого уровнякачества. Модули пришли из давнего прошлого, когда [[w:программирование|программирование]] только развивалось. Возникла надобность разбивать программы на логические части, каждую из которых создавал бы отдельный разработчик или коллектив.
 
В [[w:Haskell|Хаскеле]] также есть понятие модуля. Но более завораживающее неофитовновичков явление в языке есть понятие '''монадымонада'''. В этой лекции будут подробно рассмотрены оба понятия.
 
== Модули ==
 
В Хаскеле модули несут двоякое назначение: с одной стороны, они необходимы для контроля за пространством имён (как, собственно, и во всех других языках программирования), с другой стороны, при помощи модулей можно создавать абстрактные типы данных.
 
Определить модуль в Хаскеле достаточно просто. ИменемИмя можетмодуля бытьсостоит любойиз символлюбых символов, начинаетсялишь имяначинаться оно толькодолжно с прописной буквы. Дополнительно имяИмя модуля никак не связано с файловой системой (как, например, в [[w:Паскаль|Паскале]] и в [[w:Java|Java]]), то есть имя файла, содержащего модуль, может быть неотлично такимот же, как и названиеназвания модуля. На самом; делетакже, в одном файле может быть несколько модулей, так какибо модуль — это всего лишь навсего декларация самого высокого уровня.
 
Как известно, на верхнем уровне модуля в Хаскеле может быть множество деклараций (описаний и определений): типы, классы, данные, функции. Однако один вид деклараций должен стоять в модуле на первом месте (если этот вид деклараций вообще используется). Речь идет о включении в модуль других модулей;, длячто этогоделается используется служебное словословом <code>import</code>. Остальные определения могут появляться в любой последовательности.
 
Определение модуля должно начинаться со служебного слова <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>Tree</code> — ничего(не страшногобеда, что имя типа совпадает с названием модуля,: в данном случаездесь они находятся в различных пространствах имён) и одна функция: (<code>fringe)</code>. Здесь модуль <code>Tree</code> явно экспортирует тип <code>Tree</code> (вместе со своими подтипами <code>Leaf</code> и <code>Branch</code>) и функцию <code>fringe</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>qualified</code>, при помощи которого определяются те импортируемые модули, имена объектов в которых приобретают вид: <code><Имя Модуля>.<Имя Объекта></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>m a >>= \v  m b)</code> комбинирует монадическое значение <code>m a</code>, содержащее объект типа <code>a</code>, с функцией, которая оперирует значением <code>v</code> типа <code>a</code> и возвращает результат типа <code>m b</code>. Результатом же комбинации будет монадическое значение типа <code>m b</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> не имеет нулевого элемента, поэтому монада IO является экземпляром только класса <code>Monad</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'''
-- Выражение 1 ----------------------------------------------------------------------------------
<code>[(x, y) | x <- [1, 2, 3], y <- [1, 2, 3], x /= y]</code>
'''Выражение 2'''
-- Выражение 2-----------------------------------------------------------------------------------
<code>do x <- [1, 2, 3]
y <- [1, 2, 3]
True <- return (x /= y)
return (x, y)</code>
'''Выражение 3'''
-- Выражение 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 :
== Упражнения ==
 
#1. Какое интуитивное понимание можно вложить в понятие «монада»?
#1. Какой практический смысл имеет применение монад в функциональном программировании?
 
== Ответы для самопроверки ==