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

Содержимое удалено Содержимое добавлено
Нет описания правки
м Множество правок
Строка 1:
{{ОФП Содержание}}
 
__TOC__
 
Языки программирования не обходятся без понятия [[w:модуль (программирование)|модуля]], разве что за исключением языков самого [[w:Низкоуровневый язык программирования|низкого уровня]], да и те последнее время обретают некоторые [[w:Высокоуровневый язык программирования|высокоуровневые]] свойства. Модули появились, когда [[w:программирование|программирование]] только зарождалось и возникла надобность разбивать программы на логические части, каждую из которых создавал бы отдельный разработчик или коллектив.
 
В этой лекции освещается, как в [[w:Haskell|Хаскеле]] работают модули и более интересное новичкам явление '''[[w:Монада (математика)|монады''']].
 
== Модули ==
 
В Хаскеле у модулей двоякое назначение: с одной стороны, — контроль за [[w:Пространство имён (программирование)|пространством имён]] (как и во всех других языках), с другой, — создание абстрактных [[w:ТипАбстрактный тип данных|абстрактных типов данных]].
 
Определить модуль просто: имя состоит из любых символов, лишь начинаться оно должно с прописной буквы. Имя модуля никак не связано с [[Файловая система|файловой системой]] (как, например, в [[w:Паскаль (язык программирования)|Паскале]] и вили [[w:Java|Java]]): файл, содержащий описание модуля, может называться отлично от самого модуля; также, он может содержать несколько модулей, ибо модуль есть лишь декларация самого высокого уровня.
 
На верхнем уровне модуля в Хаскеле может быть множество деклараций (описаний и определений): [[w:Тип данных|типы]], [[w:Класс (программирование)|классы]], [[w:Данные (вычислительная техника)|данные]], [[w:Функция (программирование)|функции]]. Один вид деклараций, если он вообще используется, должен стоять в модуле на первом месте: это включение в модуль других модулей, что делается словом <code>import</code>. Другие определения могут появляться в любой последовательности.
 
Определение модуля должно начинаться со служебного слова <code>module</code>. Ниже приведено определение модуля <code>Tree</code>:
Строка 19 ⟶ 20 :
data Tree a = Leaf a
| Branch (Tree a) (Tree a)
fringe :: Tree a ->&gt; [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
Строка 35 ⟶ 36 :
main = print (fringe (Branch (Leaf 1) (Leaf 2)))</code>
 
В приведенном примере видно, что модуль <code>Main</code> импортирует модуль <code>Tree</code>, причём в декларации <code>import</code> явно описано, какие именно объекты импортируются из модуля <code>Tree</code>. Если это описание опустить, то импортироваться будут все объекты, которые модуль экспортирует, то есть в данном случае можно было просто написать: <code>import Tree</code>.
 
В обычной практике один модуль импортирует несколько других, но при этом в импортируемых модулях есть объекты с одинаковым именем. Естественно, что в этом случае возникает конфликт имён. Чтобы этого избежать в Хаскеле существует специальное служебное слово <code>qualified</code>, при помощи которого определяются те импортируемые модули, имена объектов в которых приобретают вид: <code><&lt;Имя Модуля>&gt;.<&lt;Имя Объекта>&gt;</code>, то есть для того, чтобы обратиться к объекту из квалифицированного модуля, перед его именем необходимо написать имя модуля:
 
<code>module Main where
Строка 43 ⟶ 44 :
import qualified Tree
main = print (Tree.fringe (Tree.Leaf ’a’'a'))</code>
 
Использование такого синтаксиса полностью лежит на совести [[w:Программист|программиста]]. Некоторым нравится полная определённость, которую привносят квалифицированные имена, и они используют их в ущерб размеру программ. Другим нравится использовать короткие мнемонические имена, и они используют квалификаторы (имена модулей) только по необходимости.
 
=== Абстрактные типы ===
 
В Хаскеле модуль является единственным способом создать так называемые абстрактные типы данных, то есть такие, в которых скрыто представление типа, но открыты только специфические операции над созданным типом, набор которых вполне достаточен для работы с типом. Например, хотя тип <code>Tree</code> является достаточно простым, его всевсё-таки лучше сделать абстрактным типом, то есть скрыть то, что <code>Tree</code> состоит из <code>Leaf</code> и <code>Branch</code>. Это делается следующим образом:
 
<code>module TreeADT (Tree, leaf, branch, cell, left, right, isLeaf) where
Строка 56 ⟶ 57 :
| Branch (Tree a) (Tree a)
leaf = Leaf
branch = Branch
cell (Leaf a) = a
left (Branch l r) = l
right (Branch l r) = r
isLeaf (Leaf _) = True
isLeaf _ = False</code>
 
Видно, что к внутренностям типа <code>Tree</code> внешний пользователь (программист) может пробраться, только используя определённые функции. Впоследствии, когда создатель этого модуля захочет изменить представление типа (например, [[w:Оптимизация (вычислительная техника)|оптимизировать]] его), ему необходимо будет изменить и функции, которые оперируют полями типа <code>Tree</code>. В свою очередь, программист, использовавший в своей программе тип <code>Tree</code>, ничего менять не будет, ибо его программа всевсё так же останется работоспособной.
 
=== Другие аспекты использования модулей ===
Строка 70 ⟶ 71 :
Далее приводятся дополнительные аспекты системы модулей в Хаскеле:
 
* В декларации импорта (<code>import</code>) можно выборочно спрятать некоторые из экспортируемых объектов служебным словом <code>hiding</code>. Это бывает полезным для явного исключения определений некоторых объектов из импортируемого модуля.
* При импорте можно определить псевдоним модуля для квалификации именимён, экспортируемых из него объектов. Для этого используется служебное слово <code>as</code>. Это может быть полезным для того укорачивания имен модулей.
* Все программы неявно импортируют модуль <code>Prelude</code>. Если сделать явный импорт этого модуля, то в его декларации возможно скрыть некоторые объекты, чтобы впоследствии их переопределить.
* Все декларации <code>instance</code> неявно экспортируются и импортируются всеми модулями.
* Методы классов могут быть так же как и подтипы данных перечислены в скобках после имени соответствующего класса во время декларации экспорта/импорта.
 
== Монады ==
 
Многие новички в [[w:Функциональное программирование|функциональном програмировании]] бывают часто озадачены понятием монады в Хаскеле. Но монады очень часто встречаются в языке: так, например, система ввода/-вывода основана именно на понятии монады, а стандартные библиотеки содержат целые модули, посвящённые монадам. Необходимо отметить, что понятие монады в Хаскеле основано на [[w:теорияТеория категорий|теории категорий]], но чтобы не вдаваться в абстрактную математику, далее будет представлено интуитивное понимание монад.
 
Монады суть типы, представляющие собой экземпляры одного из следующих монадических классов: <code>Functor</code>, <code>Monad</code> и <code>MonadPlus</code>. Ни один из этих классов не может быть предком для другого класса, то есть монадические классы ненаследуемы. В модуле <code>Prelude</code> определены три монады: <code>IO</code>, <code>[]</code> и <code>Maybe</code>, то есть список также является монадой.
 
Математически монада определяется через набор правил, которые связывают операции, производимые над монадой. Эти правила дают интуитивное понимание того, как должна использоваться монада и какова еееё внутренняя структура. Для конкретизации далее рассматривается класс <code>Monad</code>, в котором определены две базовые операции и одна функция:
 
<code>class Monad m where
(>>&gt;&gt;=) :: m a ->&gt; (a ->&gt; m b) ->&gt; m b
(>>&gt;&gt;) :: m a ->&gt; m b ->&gt; m b
return :: a ->&gt; m a
fail :: String ->&gt; m a
m >>&gt;&gt; k = m >>&gt;&gt;= \_ ->&gt; k</code>
 
Две операции <code>(>>&gt;&gt;=)</code> и <code>(>>&gt;&gt;)</code> — это операции связывания. Они комбинируют два монадических значения, тогда как функция <code>return</code> преобразует переданное значение некоторого типа <code>a</code> в монадическое значения типа <code>m a</code>. Сигнатура операции (<code>>>(&gt;&gt;)</code>) помогает понять операцию связывания: выражение <code>m a >>&gt;&gt;= \v -&gt; 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>(>>&gt;&gt;)</code> используется тогда, когда функция не использует значения, полученного первым монадическим [[w:Операнд|операндом]].
Точное значение операция связывания конечно же зависит от конкретной реализации монады. Так, например, тип <code>IO</code> определяет операцию <code>(>>&gt;&gt;=)</code> как последовательное выполнение двух её операндов, а результат выполнения первого операнда последовательно передаетсяпередаётся во второй. Для двух других встроенных монадических типов (списки и <code>Maybe</code>) эта операция определена как передача нуля или более [[w:Параметр (программирование)|параметров]] из одного вычислительного процесса в следующий.
 
В Хаскеле определено служебное слово, на уровне языка поддерживающее использование монад:, — слово <code>do</code>, понимание которого можно увидеть в следующих правилах его применения:
 
<code>do e1 ; e2 = e1 >>&gt;&gt; e2
do p <&lt;- e1 ; e2 = e1 >>&gt;&gt;= \p ->&gt; e2</code>
 
Первое выражение выполняется всегда (переноса значений из первого операнда во второй не производится). Во втором выражении может произойти ошибка, в этом случае производится вызов функции <code>fail</code>, которая также определена в классе <code>Monad</code>. Поэтому более точное определение служебного слова <code>do</code> для второго случая будет выглядеть так:
 
<code>do p <&lt;- e1 ; e2 = e1 >>&gt;&gt;= (\v ->&gt; case v of
p ->&gt; e2
_ ->&gt; fail ”s”"s")</code>
 
где <code>s</code> — это строка, которая может определять местоположение оператора <code>do</code> в программе, а может просто нести некоторую семантическую нагрузку. Например, в монаде <code>IO</code> действие <code>(‘a’'a' -&gt; getChar)</code> вызовет функцию <code>fail</code> в том случае, если считанный символ не является символом <code>‘a’'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 ->&gt; m a ->&gt; m a</code>
 
Нулевой элемент в этом классе монад подчиняется следующим правилам:
 
<code>m >> &gt;&gt;= \x ->&gt; mzero = mzero
mzero >>&gt;&gt;= m = mzero</code>
 
Например, для списков нулевым элементом является пустой список <code>[]</code>, а операцией «<code>+</code>» — конкатенация списков. Поэтому монада списков является экземпляром класса <code>MonadPlus</code>. С другой стороны, монада <code>IO</code> не имеет нулевого элемента, поэтому является экземпляром только класса <code>Monad</code>.
 
=== Встроенные монады ===
 
Для конкретизации полученных сведений необходимо рассмотреть что-нибудь более приземленноеприземлённое. Так как списки являются монадами и в то же время списки достаточно скрупулезно изучены, они являются отличным материалом, на котором можно рассмотреть практическое применение механизма монад.
 
Для списков операция связывания обретает смысл в соединении вместе набора операций, производимых над каждым элементом списка. При использовании со списками сигнатура операции <code>(>>&gt;&gt;=)</code> обретает следующий вид:
 
<code>(>>&gt;&gt;=) :: [a] ->&gt; (a ->&gt; [b]) ->&gt; [b]</code>
 
Это обозначает, что дан список значений типа <code>a</code> и функция, которая проецирует значение типа <code>a</code> на список значений типа <code>b</code>. Связывание применяет функцию к каждому элементу данного списка значений типа <code>a</code> и возвращает полученный список значений типа <code>b</code>. Эта операция уже известна: определители списков работают именно таким образом. То значит, что следующие три выражения абсолютно одинаковы:
 
'''Выражение 1'''
 
<code>[(x, y) | x <&lt;- [1, 2, 3], y <&lt;- [1, 2, 3], x /= y]</code>
'''Выражение 2'''
 
<code>do x <- [1, 2, 3]
<code>do yx < &lt;- [1, 2, 3]
y &lt;- [1, 2, 3]
True <- return (x /= y)
True &lt;- return (x, /= y)</code>
True <- return (x /=, y)</code>
'''Выражение 3'''
 
<code>[1, 2, 3] >>&gt;&gt;= (\x ->&gt; [1, 2, 3] >>&gt;&gt;= (\y ->&gt; return (x /= y) >>&gt;&gt;=
(\r ->&gt; case r of
True ->&gt; return (x, y)
_ ->&gt; fail ””"")))</code>
 
Какое выражение использовать в написании программ — выбирать программисту.
Строка 151 ⟶ 154 :
== Упражнения ==
 
1.# Как можно интуитивно понять явление монады?
2.# Какой практический смысл у монад в функциональном программировании?
 
2. Какой практический смысл у монад в функциональном программировании?
 
== Ответы для самопроверки ==
 
1. Первое, что приходит в голову, — это то, что монада — это контейнерный тип. Ведь действительно, список — это контейнерный тип, т.&nbsp;к.так как внутри списка содержатся элементы другого типа. Именно это и показано в определении монадического типа:
 
<code>class Monad m where
(>>&gt;&gt;=) :: m a ->&gt; (a ->&gt; m b) ->&gt; m b
(>>&gt;&gt;) :: m a ->&gt; m b ->&gt; m b
return :: a ->&gt; m a
fail :: String ->&gt; m a</code>
 
Запись «<code>m a</code>» как бы показывает, что тип <code>a</code> (необходимо чётко помнить, что при определении классов и других типов данных символы типа <code>a</code>, <code>b</code> и&nbsp;т.&nbsp;д. так далее обозначают переменные типов) обрамлён монадическим типом <code>m</code>. Однако в реальности физическое обрамление доступно только для монадического типа «список», т.&nbsp;к.так как его обозначение в виде квадратных скобок пошло традиционно. В строгой нотации Хаскела нужно было бы писать что-нибудь вроде: <code>List (1 2 3 4 5)</code> — это список <code>[1, 2, 3, 4, 5]</code>.
 
2. Применение монад в функциональных языках — это по существу возвращение к императивности. Ведь операции связывания (<code>>>(&gt;&gt;=)</code>) и (<code>>>(&gt;&gt;)</code>) предполагают последовательное выполнение связанных выражений с передачей или без результатов вычисления. Т.&nbsp;е.То есть монады — это императивное ядро внутри функциональных языков. С одной стороны, это идёт в разрез с теорией функционального програмирования, где отрицается понятие императивности, но, с другой стороны, некоторые задачи решаются только при помощи императивных принципов. И опять же, Хаскел предоставляет удивительную возможность по генерации списков, но это только благодаря тому, что сам тип «список» выполнен в виде монады.