Некоторые вопросы о монадах в Haskell

Я изучаю монады и задаю несколько вопросов.

Вот где я сейчас. Пожалуйста, поправьте меня, где я ошибаюсь.

  • Символ >>= - это инфиксный оператор. Операторы Infix - это функции, которые принимают два аргумента (левая и правая) и возвращают значение.

  • Символ >>= называется оператором связывания и имеет подпись Monad m => m t -> (t -> m u) -> m u. Тем не менее, типы, похоже, не выстраиваются здесь. Мы получаем значение типа m t, а второй аргумент - это функция, которая принимает t. (Я не вижу, как подключить точки.)

  • Это должно означать, что функция связывания каким-то образом может удалить m из m t, чтобы получить t и передать ее функции.

Вот мои вопросы:

  • Является ли возможность удалить m из m t то, что возможно только внутри такого оператора привязки. Имеет ли этот оператор привязки какие-то особые привилегии или что-то еще?

  • Что он должен делать с изменениями состояния? Я понимаю (я думаю), что цель монадов - "обернуть" побочные эффекты, чтобы они были изолированы от остальной части программы. Но какова роль оператора связывания в этом?

Ответ 1

Является ли возможность удалить "M" из "M t", что возможно только внутри такого оператора привязки. Имеет ли этот оператор привязки какие-то особые привилегии или что-то еще?

Связывание никак не является частным случаем, но обычно оно будет определено в том же модуле, что и тип данных монадов. Поэтому он может знать о (и использовать) детали, которые не экспортируются модулем. Обычный случай заключается в том, что модуль экспортирует тип данных, но не его конструкторы или другие сведения о внутренней структуре типов. Затем для кода, который использует модуль, внутренние работы типа данных невидимы и что код не может напрямую изменять значения этого типа.

Против этих функций, определенных внутри модуля, как, например, некоторый оператор привязки >>=, можно получить доступ к тем, что им нравится, из модуля, в котором они определены. Таким образом, такие функции могут выполнять вещи, которые "внешние" функции не могут выполнять.

Частным случаем является монада IO, поскольку он не определен модулем, а встроен в систему/компилятор времени выполнения. Здесь компилятор знает о внутренних деталях его реализации и предоставляет такие функции, как IO >>=. Реализации этих функций действительно являются особо привилегированными, поскольку они живут "вне программы", но это особый случай, и этот факт не должен наблюдаться изнутри Haskell.

Что он должен делать с изменениями состояния? Я понимаю (я думаю), что цель монадов - "обернуть" побочные эффекты, чтобы они были изолированы от остальной части программы. Но какова роль оператора связывания в этом?

На самом деле не обязательно иметь дело с изменениями состояния, это всего лишь одна проблема, с которой можно справиться с помощью потоков. Монада IO используется для выполнения IO в определенном порядке, но обычно монады - это просто способы комбинирования функций.

Обычно монада (в частности, функция связывания) определяет способ, которым некоторые функции должны составляться вместе с большими функциями. Этот метод объединения функций абстрагируется в монаде. Как точно это объединение работает или почему вы хотите объединить функции таким образом, не важно, монада просто определяет способ объединения определенных функций определенным образом. (См. Также этот "Monads для программистов на С#" , где я в основном повторяю это несколько раз с примерами.)

Ответ 2

- это возможность удалить "M" из "M t", что возможно только внутри такого оператора привязки.

Ну, это, безусловно, возможно внутри оператора bind, так как его тип указывает:

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

Функция "run" для вашей монады также может сделать это (вернуть чистое значение из вашего вычисления).

Цель монадов - "обернуть" побочные эффекты, чтобы они были изолированы от остальной части программы.

Хм. Нет, монады позволяют моделировать понятия вычисления. Побочные эффекты - это всего лишь одно такое понятие, как состояние, обратное отслеживание, продолжение, concurrency, транзакции, необязательные результаты, случайные результаты, возвращаемое состояние, недетерминированность... все из которых можно охарактеризовать как монаду

Предполагается, что IO-монада - это то, о чем вы говорите. Это немного странная монада - она ​​генерирует последовательности абстрактных изменений в мировое состояние, которые затем оцениваются по времени выполнения. Привязка просто позволяет нам упорядочивать вещи в правильном порядке в монаде IO - и компилятор затем переводит все эти секвенированные действия, изменяющие мир, в императивный код, который изменяет это состояние машины.

Это очень специфично для монады IO, хотя и не монады вообще.

Ответ 3

Ниже приведено определение типа-класса Monad.

class  Monad m  where

    (>>=)       :: forall a b. m a -> (a -> m b) -> m b
    (>>)        :: forall a b. m a -> m b -> m b
    return      :: a -> m a
    fail        :: String -> m a

    m >> k      = m >>= \_ -> k
    fail s      = error s

Каждый тип-тип класса Monad определяет собственную функцию >>=. Ниже приведен пример из экземпляра типа Maybe:

instance  Monad Maybe  where

    (Just x) >>= k      = k x
    Nothing  >>= _      = Nothing

    (Just _) >>  k      = k
    Nothing  >>  _      = Nothing

    return              = Just
    fail _              = Nothing

Как мы видим, поскольку версия Maybe >>= специально определена для понимания экземпляра типа Maybe и потому, что она определена в месте, имеющем законный доступ к конструкторам данных data Maybe a Nothing и Just a версия Maybe >>= может развернуть a в Maybe a и передать их.

Чтобы работать с примером, мы можем взять:

x :: Maybe Integer
x = do a <- Just 5
       b <- Just (a + 1)
       return b

De-sugared, do-notation становится:

x :: Maybe Integer
x = Just 5        >>= \a ->
    Just (a + 1)  >>= \b ->
    Just b

Что оценивается как:

  =                  (\a ->
    Just (a + 1)  >>= \b ->
    Just b) 5

  = Just (5 + 1)  >>= \b ->
    Just b

  =                  (\b ->
    Just b) (5 + 1)

  = Just (5 + 1)

  = Just 6

Ответ 4

Типы выстраиваются в линию, как ни странно. Вот как.

Помните, что монада также является функтором. Для всех функторов определена следующая функция:

fmap :: (Functor f) => (a -> b) -> f a -> f b

Теперь вопрос: действительно ли эти типы выстраиваются в линию? Ну да. Если задана функция от a до b, то, если у нас есть среда f, в которой доступна a, у нас есть среда f, в которой доступна b.

По аналогии с силлогизмом:

(Functor Socrates) => (Man -> Mortal) -> Socrates Man -> Socrates Mortal

Теперь, как вы знаете, монада - это функтор с привязкой и возвратом:

return :: (Monad m) => a -> m a
(=<<) :: (Monad m) => (a -> m b) -> m a -> m b

Возможно, вы не знаете, что это эквивалентно, это функтор, снабженный возвратом и соединением:

join :: (Monad m) => m (m a) -> m a

Посмотрите, как мы снимаем m. С монадой m вы не всегда можете получить от m a до a, но вы всегда можете получить от m (m a) до m a.

Теперь рассмотрим первый аргумент (=<<). Это функция типа (a -> m b). Что происходит, когда вы передаете эту функцию на fmap? Вы получаете m a -> m (m b). Итак, "отображение" над m a с функцией a -> m b дает вам m (m b). Обратите внимание, что это точно так же, как тип аргумента join. Это не случайность. Разумная реализация "bind" выглядит так:

(>>=) :: m a -> (a -> m b) -> m b
x >>= f = join (fmap f x)

Фактически, bind и join могут быть определены в терминах друг друга:

join = (>>= id)

Ответ 6

Я понимаю (я думаю), что цель монадов - "обернуть" побочные эффекты, чтобы они были изолированы от остальной части программы.

На самом деле это немного более тонко, чем это. Монады позволяют нам моделировать последовательность в очень общем виде. Часто, когда вы разговариваете с экспертом по домену, вы обнаруживаете, что они говорят что-то вроде "сначала мы пробуем X". Затем мы пытаемся выполнить Y, и если это не сработает, мы попробуем Z ". Когда вы приходите, чтобы реализовать что-то подобное на обычном языке, вы обнаружите, что он не подходит, поэтому вам нужно написать много дополнительного кода, чтобы охватить все, что имел в виду эксперт домена под словом" then".

В Haskell вы можете реализовать это как монаду с "затем", переведенным в оператор привязки. Так, например, я однажды написал программу, в которой элемент должен был быть назначен из пулов в соответствии с определенными правилами. Для случая 1 вы взяли его из пула X. Если это было пусто, вы перешли к пулу Y. Для случая 2 вам нужно было взять его прямо из пула Y. И так далее для дюжины или около того случаев, включая некоторые, где вы взяли последний раз использовался либо из пула X, либо из Y. Я написал специальную монаду специально для этой работы, чтобы я мог написать:

case c of
   1: do {try poolX; try poolY}
   2: try poolY
   3: try $ lru [poolX, poolY]

Он работал очень хорошо.

Конечно, это включает обычные модели секвенирования. Мода IO - это модель, которой обладают все другие языки программирования; его просто в Haskell - это явный выбор, а не часть среды. Монада ST дает вам мутацию памяти IO, но без фактического ввода и вывода. С другой стороны, государственная монада позволяет ограничить ваше состояние одним значением именованного типа.

Для чего-то действительно изгибающегося мозга, см. это сообщение в блоге о монаде с отсталым состоянием. Государство распространяется в противоположном направлении к "исполнению". Если вы думаете об этом, как о государственной монаде, исполняющей одну инструкцию, а затем о следующем, тогда "put" отправит значение состояния назад во времени до любого предшествующего "get". Фактически происходит то, что создается взаимно-рекурсивная функция, которая заканчивается только при отсутствии парадоксов. Я не совсем уверен, где использовать такую ​​монаду, но это иллюстрирует точку зрения о том, что монады являются моделями вычислений.

Если вы не готовы к этому, просто подумайте о привязке как к перегружаемой точке с запятой. Это доставит вам долгий путь.