Теория монад и хаскелл - PullRequest
       31

Теория монад и хаскелл

35 голосов
/ 17 декабря 2010

Большинство учебных пособий, похоже, дают много примеров монад (IO, состояние, список и т. Д.), А затем ожидают, что читатель сможет абстрагировать общий принцип, и затем они упоминают теорию категорий. Я не склонен учиться очень хорошо, пытаясь обобщить на примерах, и я хотел бы понять с теоретической точки зрения, почему эта модель так важна.

Судя по этой теме: Может кто-нибудь объяснить монады? это распространенная проблема, и я попытался просмотреть большинство предложенных руководств (за исключением видеороликов Брайана Бека, которые не воспроизводятся на моем компьютере с Linux):

Кто-нибудь знает учебник, который начинается с теории категорий и объясняет IO, состояние, списочные монады в этих терминах? Вот моя неудачная попытка сделать это:

Насколько я понимаю, монада состоит из тройки: эндофунктора и двух естественных преобразований.

Функтор обычно отображается с типом: (a -> b) -> (m a -> m b) Я включил вторую скобку, чтобы подчеркнуть симметрию.

Но, это endofunctor, поэтому не должны ли домен и кодомен быть такими же, как этот?:

(a -> b) -> (a -> b)

Я думаю, что ответ заключается в том, что домен и кодомен оба имеют тип:

(a -> b) | (m a -> m b) | (m m a -> m m b) и так далее ...

Но я не совсем уверен, работает ли это или согласуется с данным определением функтора?

Когда мы переходим к естественному преобразованию, становится еще хуже. Если я правильно понимаю, естественное преобразование - это функтор второго порядка (с определенными правилами), который является функтором от одного функтора к другому. Так как мы определили функтор выше, общий тип естественных преобразований будет: ((a -> b) -> (m a -> m b)) -> ((a -> b) -> (m a -> m b))

Но фактические естественные преобразования, которые мы используем, имеют тип:

a -> m a

m a -> (a -> m b) -> m b

Эти подмножества общего вида указаны выше? и почему они естественные преобразования?

Martin

Ответы [ 9 ]

18 голосов
/ 17 декабря 2010

Быстрый отказ от ответственности: я немного шаткую по теории категорий в целом, хотя у меня складывается впечатление, что вы хотя бы немного знакомы с ней.Надеюсь, я не буду делать из этого слишком много хеша ...

Кто-нибудь знает учебник, который начинается с теории категорий и объясняет IO, состояние, списочные монады в этих терминах?

Прежде всего, пока игнорируйте IO, он полон темной магии.Он работает как модель императивных вычислений по тем же причинам, по которым State работает для моделирования вычислений с состоянием, но в отличие от последних IO является черным ящиком, не имеющим возможности вывести монадическую структуру извне.

Функтор обычно отображается с типом: (a -> b) -> (ma -> mb) Я включил вторую скобку, чтобы подчеркнуть симметрию.

Но это эндофункторпоэтому разве домен и кодомен не должны быть такими же, как этот ?:

Я подозреваю, что вы неверно истолковываете, как переменные типа в Haskell связаны с понятиями теории категорий.

Прежде всего, да, это определяет endofunctor, для категории типов Haskell.Однако переменная типа, такая как a, не относится к этой категории;это переменная, которая (неявно) определяется количественно по всем объектам в категории.Таким образом, тип (a -> b) -> (a -> b) описывает только endofunctors, которые отображают каждый объект на себя .

Конструкторы типов описывают инъективную функцию на объектах, где элементы кодомена конструктора не могут быть описаны любымозначает, кроме как в качестве приложения конструктора типа.Даже если два типа конструкторов дают изоморфные результаты, результирующие типы остаются разными.Обратите внимание, что конструкторы типов в общем случае не являются функторами.

Переменная типа m в сигнатуре функтора представляет конструктор типа с одним аргументом.Вне контекста это обычно читается как универсальное количественное определение, но это неверно в этом случае, так как такая функция не может существовать.Скорее, определение класса типа связывает m и позволяет определить такие функции для определенных конструкторов типов.

Результирующая функция, следовательно, говорит, что для любого конструктора типов m, которыйопределил fmap, для любых двух объектов a и b и морфизма между ними мы можем найти морфизм между типами, заданными путем применения m к a и b.

Обратите внимание, что, хотя приведенное выше, конечно, определяет эндофунктор для Hask , он даже не является достаточно общим для описания всех таких эндофункторов.

Но фактические естественные преобразования, которые мы используем, имеют тип:

a -> ma

ma -> (a -> mb) -> mb

Являются ли эти подмножестваобщей формы выше?и почему они естественные преобразования?

Ну, нет, это не так.Естественное преобразование - это примерно функция (а не функтор) между функторами.Два естественных преобразования, которые задают монаду M, выглядят как I -> M, где I - функтор тождества, а M ∘ M -> M, где - функтор композиции.В Haskell у нас нет хорошего способа работать напрямую с настоящим функтором идентификации или с композицией функтора.Вместо этого мы отбрасываем функтор идентификации, чтобы получить только (Functor m) => a -> m a для первого, и записываем приложение-конструктор вложенных типов как (Functor m) => m (m a) -> m a для второго.

Первый из них, очевидно, return;вторая - это функция с именем join, которая не является частью класса типа.Тем не менее, join можно записать в терминах (>>=), и последний чаще используется в повседневном программировании.


Что касается конкретных монад, если вы хотитеболее математическое описание, вот краткий пример:

Для некоторого фиксированного типа S рассмотрим два функтора F и G, где F (x) = (S, x) и G (x) = S ->x (Надеюсь, должно быть очевидно, что это действительно допустимые функторы).

Эти функторы также являются присоединениями; рассмотрим естественные преобразования unit :: x -> G (F x) и counit :: F (G x) -> x. Расширение определений дает нам unit :: x -> (S -> (S, x)) и counit :: (S, S -> x) -> x. Типы предполагают применение неиспользуемых функций и построение кортежей; не стесняйтесь проверить, что они работают, как ожидалось.

Присоединение порождает монаду по составу функторов, поэтому, взяв G ∘ F и расширив определение, мы получим G (F x) = S -> (S, x), что является определением State монада. unit для присоединения, конечно, return; и вы должны иметь возможность использовать counit для определения join.

15 голосов
/ 17 декабря 2010

Эта страница делает именно это. Я думаю, что ваша основная путаница заключается в том, что класс на самом деле не делает Type функтором, но он определяет функтор из категории типов Haskell в категорию этого типа.

После обозначения ссылки, предполагая, что F является функтором Haskell, это означает, что существует функтор из категории Hask в категорию F.

10 голосов
/ 24 декабря 2010

Грубо говоря, Haskell объединяет свою теорию категорий только в одну категорию, чьи объекты являются типами Haskell и чьи стрелки являются функциями между этими типами.Это определенно не язык общего назначения для моделирования теории категорий.

(математический) функтор - это операция, превращающая вещи в одной категории в вещи в другой, возможно, совершенно другой категории.Тогда endofunctor - это функтор, который имеет одинаковые исходные и целевые категории.В Haskell функтор - это операция, превращающая вещи в категории типов Haskell в другие вещи, в том числе и в категорию типов Haskell, поэтому он всегда является эндофунктором.

[Если вы следуете математической литературе,технически операция '(a-> b) -> (ma -> mb)' - это просто часть стрелки эндофунктора m, а 'm' - это часть объекта ]

Когда Хаскелеры говорят о работе «в монаде», они действительно имеют в виду работу в категории монады Клейсли.Поначалу категория монады Клейсли - это совершенно сбивающий с толку зверь, и обычно для ее объяснения требуются как минимум два цвета чернил, поэтому сделайте следующую попытку, чтобы узнать, что это такое, и посмотрите некоторые ссылки (к сожалению, википедия здесь бесполезна длявсе, кроме прямых определений).

Предположим, у вас есть монада 'm' в категории C типов Haskell.Его категория Клейсли Kl (m) имеет те же объекты, что и C, а именно типы Хаскеля, но стрелка a ~ (f) ~> b в Kl (m) является стрелкой a - (f) -> mb в C. (IЯ использовал волнистую линию в моей стрелке Клейсли, чтобы различить их).Повторим еще раз: объекты и стрелки Kl (C) также являются объектами и стрелками C, но стрелки указывают на другие объекты в Kl (C), чем в C. Если это не кажется вам странным, прочитайте его еще раз.осторожно!

Конкретно рассмотрим монаду Может быть.Его категория Клейсли - это просто набор типов Хаскеля, а стрелки a ~ (f) ~> b являются функциями a - (f) -> Maybe b.Или рассмотрим монаду (State s), у которой стрелки a ~ (f) ~> b являются функциями a - (f) -> (State sb) == a - (f) -> (s -> (s, b)),В любом случае, вы всегда пишете волнистую стрелку как сокращение для того, чтобы что-то сделать с типом кодомена ваших функций.

[Обратите внимание, что State не является монадой, потому что тип State - * -> * -> *, поэтому вам нужно указать один из параметров типа, чтобы превратить его в математическую монаду.]

Пока все хорошо, надеюсь, но предположим, что вы хотите составить стрелки a ~ (f) ~> b и b ~ (g) ~> c.Это действительно функции Хаскелла a - (f) -> mb и b - (g) -> mc, которые вы не можете составить, потому что типы не совпадают.Математическое решение состоит в том, чтобы использовать естественное преобразование «умножение» u: mm-> m монады следующим образом: a ~ (f) ~> b ~ (g) ~> c == a - (f) -> mb -(mg) -> mmc - (u_c) -> mc, чтобы получить стрелку a-> mc, которая является стрелкой Клейсли a ~ (f; g) ~> c, как требуется.

Возможно, конкретный пример помогаетВот.В монаде Maybe вы не можете составлять функции f: a -> Maybe b и g: b -> Maybe c напрямую, но подняв g до

Maybe_g :: Maybe b -> Maybe (Maybe c)
Maybe_g Nothing = Nothing
Maybe_g (Just a) = Just (g a)

и используя «очевидный»

u :: Maybe (Maybe c) -> Maybe c
u Nothing = Nothing
u (Just Nothing) = Nothing
u (Just (Just c)) = Just c

Вы можете сформировать композицию u . Maybe_g . f, которая является функцией a -> Может быть, c, которую вы хотели.

В монаде (State s) она похожа, но более сложна: учитывая две монадические функции a~ (f) ~> b и b ~ (g) ~> c, которые на самом деле являются a - (f) -> (s -> (s, b)) и b - (g) -> (s -> (s, в)) под капотом вы составляете их, поднимая g в

State_s_g :: (s->(s,b)) -> (s->(s,(s->(s,c))))
State_s_g p s1 = let (s2, b) = p s1 in (s2, g b)

, затем применяете естественное преобразование «умножение» u, которое равно

u :: (s->(s,(s->(s,c)))) -> (s->(s,c))
u p1 s1 = let (s2, p2) = p1 s1 in p2 s2

, что) включает конечное состояние f в начальное состояние g.

В Haskell это оказывается немного неестественным способом работы, так что вместо этого есть функция (>>=), которая в основном делает то же самое, что и вы, но таким образом, чтобы ее было проще реализовать и использовать. Это важно: (>>=) - это не естественная трансформация 'u'.Вы можете определить каждый в терминах другого, поэтому они эквивалентны, но это не одно и то же.Версия 'u' на Haskell написана join.

. В этом определении категорий Клейсли отсутствует еще одна вещь - это тождество каждого объекта: ~ (1_a) ~> a, которое на самом деле является - (n_a) -> ma, где n - естественное преобразование «единицы».Это написано return на Хаскеле, и, кажется, не вызывает такой путаницы.

Я изучил теорию категорий, прежде чем пришел в Хаскелл, и у меня тоже возникли трудности с несоответствием между тем, что математики называютмонада и как они выглядят в Хаскеле.С другого направления не легче!

9 голосов
/ 17 декабря 2010

Не уверен, что понимаю вопрос, но да, вы правы, монада в Хаскеле определяется как тройка:

m :: * -> * -- this is endofunctor from haskell types to haskell types!
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b

, но общее определение из теории категорий - это еще одна тройка:

m :: * -> *
return :: a -> m a
join ::  m (m a) -> m a

Это немного сбивает с толку, но не так сложно показать, что эти два определения равны.Для этого нам нужно определить join в терминах (>>=) (и наоборот).Первый шаг:

join :: m (m a) -> m a
join x = ?

Это дает нам x :: m (m a).
Все, что мы можем сделать с чем-то, что имеет тип m _, это применить к нему (>> =):

(x >>=) :: (m a -> m b) -> m b

Теперь нам нужно что-то в качестве второго аргумента для (>> =), а также из типа объединения у нас есть ограничение (x >>= y) :: ma.
Так что y здесь будет иметь тип y :: ma -> ma и id :: a -> a очень хорошо подходит:

join x = x >>= id

Другой путь

(>>=) :: ma -> (a -> mb) -> m b
(>>=) x y = ?

Где x :: m a и y :: a -> m b.Чтобы получить m b из x и y нам нужно что-то типа a.
К сожалению, мы не можем извлечь a из m a.Но мы можем заменить его чем-то другим (помните, монада также является функтором):

fmap :: (a -> b) -> m a -> m b
fmap y x :: m (m b)

И он идеально подходит в качестве аргумента для join: (>>=) x y = join (fmap y x).

4 голосов
/ 17 декабря 2010

Лучший способ взглянуть на монады и вычислительные эффекты - начать с того, откуда у Хаскелла появилось понятие о монадах для вычислительных эффектов, а затем посмотреть на Хаскелл после , и вы это понимаете. В частности, см. Эту статью: Понятия вычислений и монад , Э. Могги.

См. Также предыдущую статью Могги, в которой показано, как работают монады только для лямбда-исчисления: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.26.2787

Тот факт, что монады захватывают подстановку, среди прочего (http://blog.sigfpe.com/2009/12/where-do-monads-come-from.html),, а подстановка является ключом к лямбда-исчислению, должен дать хороший ключ к пониманию того, почему они обладают такой выразительной силой.

3 голосов
/ 18 декабря 2010

Один из способов взглянуть на IO - это рассмотреть его как странную монаду состояния. Помните, что государственная монада выглядит так:

data State s a = State (s -> (s, a))

где аргумент "s" - это тип данных, который вы хотите использовать в вычислениях. Кроме того, эта версия «State» не имеет действий «get» и «put», и мы не экспортируем конструктор.

Теперь представьте себе тип

data RealWorld = RealWorld ......

Это не имеет реального определения, но условно значение типа RealWorld содержит состояние всей вселенной за пределами компьютера. Конечно, у нас никогда не может быть значения типа RealWorld, но вы можете представить что-то вроде:

getChar :: RealWorld -> (RealWorld, Char)

Другими словами, функция «getChar» принимает состояние юниверса до нажатия кнопки клавиатуры и возвращает нажатую клавишу, а также состояние юниверса после нажатия клавиши. Конечно, проблема в том, что на предыдущее состояние мира все еще можно ссылаться, чего не может быть на самом деле.

Но теперь мы пишем это:

тип IO = State RealWorld

getChar :: IO Char

В принципе, все, что мы сделали, - обернули предыдущую версию getChar как действие состояния. Но, делая это, мы больше не можем получить доступ к значениям "RealWorld", потому что они заключены в монаду State.

Поэтому, когда программе на Haskell требуется заменить лампочку, она берет лампочку и применяет функцию «вращения» к значению RealWorld внутри IO.

3 голосов
/ 17 декабря 2010

Хотя монады изначально пришли из теории категорий, это не значит, что теория категорий является единственным абстрактным контекстом, в котором вы можете их просматривать.Другая точка зрения задается операционной семантикой .Для ознакомления ознакомьтесь с моим учебным пособием по операционной монаде .

2 голосов
/ 25 декабря 2010

Для меня, до сих пор, объяснение, которое наиболее близко связывает воедино монады в теории категорий и монады в Хаскеле, состоит в том, что монады - это мониды, чьи объекты имеют тип a-> m b.Я вижу, что эти объекты очень близки к эндофунктору, и поэтому состав таких функций связан с императивной последовательностью программных операторов.Также функции, которые возвращают функции ввода-вывода, действительны в чистом функциональном коде до тех пор, пока внутренняя функция не будет вызвана извне.

Этот элемент id является 'a -> ma', который очень хорошо вписывается, но элемент умножения - это композиция функций,должно быть:

(> =>) :: Monad m => (a -> mb) -> (b -> mc) -> (a -> mc)

Этоне совсем композиция функций, но достаточно близко (я думаю, чтобы получить истинную композицию функций, нам нужна дополнительная функция, которая превращает mb обратно в a, тогда мы получим композицию функций, если мы применим их в парах?), я не совсем уверен, какполучить от этого к этому:

(>> =) :: Monad m => ma -> (a -> mb) -> mb

У меня есть ощущение, что у меня может бытьЯ видел объяснение этому во всех материалах, которые я прочитал, не понимая его значения с первого раза, поэтому я сделаю некоторое перечитывание, чтобы попытаться (пере) найти объяснение этому.

Другойчто я хотел бы сделать, это связать воедино все различные теории категорийобъяснения: эндофунктор + 2 естественных преобразования, категория Клейсли, моноид, объектами которого являются моноиды и так далее.Мне кажется, что все эти объяснения связывают то, что они двухуровневые.То есть обычно мы относимся к объектам категории как к черным ящикам, где мы подразумеваем их свойства от их внешних взаимодействий, но здесь, кажется, необходимо пройти один уровень внутри объектов, чтобы увидеть, что происходит?Мы можем объяснить монады без этого, но только если примем явно произвольные конструкции.

Martin

0 голосов
/ 24 января 2014

См. Этот вопрос: единственные вещи, которые решает класс монад, - это цепочечные операции?

В нем я объясняю свое видение того, что мы должны различать класс Монады и отдельные типы, которые решают отдельные проблемы. Класс Monad сам по себе только решает важную проблему «цепочки операций с выбором» и делает это решение доступным для типов, являющихся его экземплярами (посредством «наследования»).

С другой стороны, если данный тип, который решает данную проблему, сталкивается с проблемой «цепочки операций с выбором», тогда он должен быть экземпляром (наследником) класса Monad.

Дело в том, что проблемы не решаются просто монадой. Это все равно, что сказать, что «колеса» решают многие проблемы, но на самом деле «колеса» только решают проблему, а вещи с колесами решают много разных проблем.

...