Что значит работать внутри монады?

Просматривая главы учебника Haskell, посвященные различным монадам, я постоянно теряюсь, когда авторы переходят от объяснения деталей bind и законов монад к фактическому использованию монад. Внезапно всплывают такие выражения, как «запуск функции в монадическом контексте» или «запуск монады». Точно так же в документации к библиотеке и в дискуссиях о стеках преобразователей монад я читал утверждения, что некоторые функции «могут быть запущены в любой монаде». Что именно означает это «работа внутри монады»?

Есть две вещи, которые я, кажется, не понимаю прямо:

  1. Монада — это класс типов с функциями (return, >>=) и законами. Таким образом, «запустить» что-то внутри монады может означать либо (а) предоставить это в качестве аргумента для return, либо (б) упорядочить его с помощью >>=. Если монада имеет тип m a, то в случае а) это что-то должно быть типа a, чтобы соответствовать типу функции return. В случае б) что-то должно быть функцией типа a -> m b, чтобы соответствовать типу функции >>=. Из этого я не понимаю, как я могу «запустить» какую-то функцию внутри произвольной монады, потому что функции, которые я последовательно использую с помощью >>=, должны иметь сигнатуру одного и того же типа, а значения, которые я поднимаю с помощью return, должны иметь определенный параметр типа монады. .
  2. Насколько я понимаю, в функциональном языке нет понятия выполнения или запуска вычисления — есть только приложение функции к некоторому аргументу и вычисление функции (замена ее на его значение). Тем не менее, многие конкретные монады поставляются с функцией run, такой как runReader, runState и т. д. Эти функции не являются частью определения монады, и они являются простыми функциями, ни в коем случае не специальными императивными операторами вне функционального ядра языка. . Так что же они "бегут"?

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


person Ulrich Schuster    schedule 15.05.2020    source источник


Ответы (3)


Авторы, пишущие книги и статьи, часто используют метафоры и менее точный язык, когда пытаются объяснить концепции. Цель состоит в том, чтобы дать читателю концептуальное представление о том, что происходит.

Я считаю, что концепция «запуска» функции относится к этой категории. Помимо IO, вы правы в том, что функции, которые вы используете для составления, скажем, [], Maybe и т. д., ничем не отличаются от других функций.

Я думаю, что идея запуска чего-то внутри монады исходит из наблюдения, что функторы являются контейнерами. Это наблюдение применимо и к монадам, поскольку все монады являются функторами. [Bool] — это контейнер логических значений, Maybe Int — это контейнер (ноль или единица) чисел. Вы даже можете думать о функторе чтения r -> a как о контейнере a значений, потому что вы можете представить, что это просто очень большая таблица поиска.

Возможность «запустить функцию внутри контейнера» полезна, потому что не все контейнеры обеспечивают доступ к своему содержимому. Опять же, IO является ярким примером, поскольку это непрозрачный контейнер.

Часто задаваемый вопрос: как вернуть чистое значение из нечистого метода. Точно так же многие новички спрашивают: как получить значение Maybe? Вы даже можете спросить: как мне получить значение из списка? В обобщенном виде вопрос звучит так: Как получить значение из монады.

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

Имейте в виду, когда речь заходит о методе bind (>>=), что хотя функция, «работающая внутри него», имеет тип a -> m b, вы также можете «запускать» «нормальную» функцию a -> b внутри монады. с fmap, потому что все экземпляры Monad также являются экземплярами Functor.

person Mark Seemann    schedule 15.05.2020
comment
Как получить значение из монады: в моем понимании, это не проблема монады как таковой, но если модуль, в котором определен тип, имеющий экземпляр класса типа монады, выставляет данные типов конструктор или нет. Если это так, я могу сопоставить его с образцом и извлечь значение, обернутое внутри этого типа. При использовании >>= мне не нужно это сопоставление с образцом, потому что оно неявно выполняется внутри >>=. - person Ulrich Schuster; 15.05.2020
comment
@UlrichSchuster Это правда, что это не проблема монады абстракции, а конкретных монад (опять же, особенно: IO). Однако вопрос не в том, экспортируются ли конструкторы данных. И [], и Maybe, и Either e экспортируют свои конструкторы данных. Как вы получаете значение от любого из них? - person Mark Seemann; 15.05.2020
comment
Ну, не обязательно есть одно значение, как вы указываете в своем сообщении в блоге blog.ploeh.dk/2019/02/04/how-to-get-the-value-out-of-the-monad. Но пока я могу сопоставлять шаблоны в конструкторах данных, я могу возиться с реализацией типа. Вот почему я также пытаюсь лучше понять абстрактные типы данных в Haskell (stackoverflow.com/questions/61638297/). - person Ulrich Schuster; 15.05.2020
comment
@UlrichSchuster Да, вы, безусловно, можете сопоставлять шаблоны в конструкторах данных [], Maybe, Either e и т. д., но тогда вам придется охватить все случаи, вручную написав код, который имеет дело с каждым случаем. fmap и >>= дают вам удобные способы составления небольших функций в рассматриваемом контейнере. Это может быть особенно ясно с Either e, где вы можете игнорировать случай Left, когда хотите сосредоточиться на счастливом пути вашего кода. - person Mark Seemann; 15.05.2020

функции, которые я упорядочиваю с помощью >>=, должны иметь одинаковую сигнатуру типа

Это только отчасти правда. В некотором монадическом контексте у нас может быть выражение

x >>= f >>= g

где

x :: Maybe Int
f :: Int -> Maybe String
g :: String -> Maybe Char

Все они должны включать одну и ту же монаду (Maybe), но обратите внимание, что не все они имеют одинаковую сигнатуру типа. Как и в случае с обычной композицией функций, вам не нужно, чтобы все возвращаемые типы были одинаковыми, достаточно, чтобы входные данные одной функции совпадали с выходными данными ее предшественницы.

person amalloy    schedule 15.05.2020

Вот простая аналогия «запустить функцию внутри контейнера» с псевдокодом:

Допустим, у вас есть некоторый тип Future[String], который представляет собой контейнер, в котором будет строка «когда-нибудь в будущем»:

val tweet: Future[String] = getTweet()

Теперь вы хотите получить доступ к строке — но вы не вырываете строку из контекста — «будущее» — вы просто используете строку «внутри контейнера»:

tweet.map { str =>
  println(str)
}

Внутри этих фигурных скобок вы находитесь «в будущем». Например:

val tweet: Future[String] = getTweet()

tweet.map { str =>
  println(str)
}

println("Length of tweet string is " + tweet.length) // <== WRONG -- you are not yet in the future

tweet.length пытается получить доступ к твиту за пределами контейнера. Таким образом, «нахождение внутри контейнера» при чтении исходного кода аналогично «нахождению внутри фигурных скобок карты (плоской карты и т. д.)». Вы погружаетесь внутрь контейнера.

tweet.map { str =>
  println("Length of tweet string is " + str.length) // <== RIGHT
}

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

person Jim Flood    schedule 17.05.2020