Что такого особенного в Monads в категории Kleisli? - PullRequest
0 голосов
/ 02 ноября 2018

Вопрос по теме

  1. Что такого особенного в монадах?

  2. bind может состоять из fmap и join, поэтому мы должны использовать монадические функции a -> m b?

В первом вопросе:

Что особенного в Монадах?

Монада - это математическая структура, которая широко используется в (чистом) функциональном программировании, в основном на Haskell. Однако существует множество других доступных математических структур, таких как, например, аппликативные функторы, сильные монады или моноиды. У некоторых есть более определенная, некоторые более общие. Тем не менее, монады гораздо более популярны. Почему это так?

Комментарий для ответа на вопрос:

Насколько я помню, Wadler популяризировал монады, и в то время идея выполнения ввода-вывода без утомительного CPS и анализа без явной передачи состояния была огромной выгодой; это было невероятно захватывающее время. А.Ф.А.И.Р., Хаскелл не делал классы конструктора, а Гофер (отец Хугса) делал. Вадлер предложил перечитать списки для монад, перегруженных списками, так что нотация поступила позже. Как только IO стал монадическим, монады стали большой новостью для начинающих, поскольку они стали основной вещью, которую нужно покупать. Аппликативы гораздо приятнее, когда вы можете, и Arrows более общие, но они пришли позже, и IO продает монады тяжело. - AndrewC 9 мая 13 года в 1: 34

Ответ @Conal:

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

Прежде всего, я согласен с ними, и я думаю, что полезность Monads главным образом вытекает из Functors, что мы можем встраивать многие функции в структуру, а Monads - это небольшое расширение для надежности композиции функций на join: M(M(X)) -> M(X) чтобы избежать вложенного типа.

Во втором вопросе:

мы должны использовать монадические функции a -> m b?

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

и много ответов типа

Мне нравится думать о таком m как о значении «план, который нужно получить», где «планы» подразумевают какое-то дополнительное взаимодействие помимо чистого вычисления.

или

В ситуациях, когда Monad не требуется, часто проще использовать Applicative, Functor или просто базовые чистые функции. В этих случаях эти вещи следует (и обычно) использовать вместо Monad. Например:

ws <- getLine >>= return . words  -- Monad
ws <- words <$> getLine           -- Functor (much nicer)

Чтобы быть ясным: если это возможно без монады, и проще и более читабельно без монады, то вы должны сделать это без монады! Если монада делает код более сложным или запутанным, чем нужно, не используйте монаду! На Haskell есть монады с единственной целью - сделать некоторые сложные вычисления более простыми, удобными для чтения и рассуждениями. Если этого не происходит, вы не должны использовать монаду .

Читая их ответы, я полагаю, что их особое отношение к Монаде возникает из-за исторического инцидента, в котором сообщество Хаскелла выбрало Монады в категории Клейсли для решения своей проблемы (IO и т. Д.)

Итак, опять же, я думаю, что полезность Monads главным образом вытекает из Functors, что мы можем встраивать многие функции в структуру, а Monads - это небольшое расширение для надежности композиции функций на join: M(M(X)) -> M(X), чтобы избежать вложенности типа.

На самом деле, в JavaScript я реализовал, как показано ниже ..

Functor

console.log("Functor");
{
  const unit = (val) => ({
    // contextValue: () => val,
    fmap: (f) => unit((() => {
      //you can do pretty much anything here
      const newVal = f(val);
    //  console.log(newVal); //IO in the functional context
      return newVal;
    })()),
  });

  const a = unit(3)
    .fmap(x => x * 2)  //6
    .fmap(x => x + 1); //7
}

Дело в том, что мы можем реализовать все, что захотим, в структуре Functor, и в этом случае я просто сделал это IO / console.log значением.

Другое дело, что делать это монады совершенно не нужно.

Монада

Теперь, на основе описанной выше реализации Functor, я добавил дополнительную функцию join: MMX => MX, чтобы избежать вложенной структуры, которая должна быть полезной для устойчивости сложной функциональной композиции.

Функциональность в точности идентична описанной выше функции Functor, и обратите внимание, что использование также идентично функции Functor fmap. Для этого не требуется «монадическая функция» для bind (композиция монад по Клейсли).

console.log("Monad");
{
  const unit = (val) => ({
    contextValue: () => val,
    bind: (f) => {
      //fmap value operation
      const result = (() => {
        //you can do pretty much anything here
        const newVal = f(val);
        console.log(newVal);
        return newVal;
      })();
      //join: MMX => MX
      return (result.contextValue !== undefined)//result is MX
        ? result //return MX
        : unit(result) //result is X, so re-wrap and return MX
    }
  });
  //the usage is identical to the Functor fmap.
  const a = unit(3)
    .bind(x => x * 2)  //6
    .bind(x => x + 1); //7
}

Законы Монады

На всякий случай, эта реализация Монады удовлетворяет законам монады, а описанный выше Функтор - нет.

console.log("Monad laws");
{
  const unit = (val) => ({
    contextValue: () => val,
    bind: (f) => {
      //fmap value operation
      const result = (() => {
        //you can do pretty much anything here
        const newVal = f(val);
        //console.log(newVal);
        return newVal;
      })();
      //join: MMX => MX
      return (result.contextValue !== undefined)
        ? result
        : unit(result)
    }
  });

  const M = unit;
  const a = 1;
  const f = a => (a * 2);
  const g = a => (a + 1);

  const log = m => console.log(m.contextValue()) && m;
  log(
    M(f(a))//==m , and f is not monadic
  );//2
  console.log("Left Identity");
  log(
    M(a).bind(f)
  );//2
  console.log("Right Identity");
  log(
    M(f(a))//m
      .bind(M)// m.bind(M)
  );//2
  console.log("Associativity");
  log(
    M(5).bind(f).bind(g)
  );//11
  log(
    M(5).bind(x => M(x).bind(f).bind(g))
  );//11

}

Итак, вот мой вопрос.

Я могу ошибаться.

Есть ли контрпример, что Функторы не могут делать то, что могут делать Монады, кроме надежности функциональной композиции путем выравнивания вложенной структуры?

Что особенного в Монаде в категории Клейсли? Кажется, что вполне возможно реализовать Monads с небольшим расширением, чтобы избежать вложенной структуры Functor и без монадических функций a -> m b, то есть сущности в категории Kleisli.

Спасибо.

редактировать (2018-11-01)

Читая ответы, я согласен, что нецелесообразно выполнять console.log внутри IdentityFunctor, который должен удовлетворять законам Функтора, поэтому я прокомментировал как код Монады.

Итак, устраняя эту проблему, мой вопрос остается в силе:

Есть ли какой-нибудь встречный пример того, что Функторы не могут сделать то, что могут сделать Монады, кроме надежности функциональной композиции путем выравнивания вложенной структуры?

Что особенного в Монаде в категории Клейсли? Кажется, что вполне возможно реализовать Monads с небольшим расширением, чтобы избежать вложенной структуры Functor и без монадических функций a -> m b, то есть сущности в категории Kleisli.

Ответ от @DarthFennec таков:

«Избегать вложенного типа» на самом деле не цель join, это просто аккуратный побочный эффект. То, как вы это делаете, звучит так, будто join просто удаляет внешний тип, но значение монады не меняется.

Я полагаю, что «Избегание вложенного типа» - это не просто аккуратный побочный эффект, но определение «соединения» Монады в теории категорий,

умножение естественного преобразования μ: T∘T⇒T монады обеспечивает для каждого объекта X морфизм μX: T (T (X)) → T (X)

монада (в информатике): отношение к монадам в теории категорий

и это именно то, что делает мой код.

С другой стороны,

Это не тот случай. join является сердцем монады, и именно это позволяет монаде делать вещи .

Я знаю, что многие люди реализуют монады в Haskell таким образом, но факт в том, что в Haskell есть Возможно функтор , у которого нет join, или есть Free monad , что join встроено с первого места в определенную структуру. Это объекты, которые пользователи определяют Функторы для делать вещи .

Следовательно,

Вы можете думать о функторе как о контейнере. Есть произвольный внутренний тип, и вокруг него внешняя структура, которая позволяет некоторой дисперсии, некоторым дополнительным значениям «украшать» ваше внутреннее значение. fmap позволяет вам работать с вещами внутри контейнера так, как вы работаете с ними в обычном режиме. Это в основном предел того, что вы можете сделать с функтором.

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

Эти наблюдения не соответствуют факту существования функтора Maybe и Свободной монады.

Ответы [ 4 ]

0 голосов
/ 02 ноября 2018

слишком длинный комментарий:

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

Кроме того, хотя я все еще не до конца понимаю ваш вопрос и утверждения, некоторый контекст, который может быть полезен: теория категорий является чрезвычайно общей и абстрактной; такие понятия, как Monad и Functor в том виде, в каком они существуют в haskell, (обязательно) несколько меньше общие и меньше абстрактные (например, понятие категории "Hask").

Как правило, чем более конкретна (менее абстрактна) вещь, тем больше у вас силы: если я скажу вам, что у вас есть транспортное средство, вы знаете, что у вас есть вещь, которая может перенести вас из одного места в другое, но вы не знаете, как быстро, вы не знаете, может ли он лететь на суше и т. д. Если я скажу вам, что у вас есть скоростной катер, тогда откроется целый большой мир вещей, которые вы можете сделать и рассуждать (вы можете использовать его для ловли рыбы, вы знаете, что она не доставит вас из Нью-Йорка в Денвер).

Когда вы говорите:

Что особенного в Монаде в категории Клейсли?

... Я полагаю, вы ошибаетесь, подозревая, что концепция Monad и Functor в haskell в некотором роде больше ограничительна по отношению к теории категорий, но, как я пытаюсь По аналогии выше, верно и обратное.

Ваш код - тот же тип ошибочного мышления: вы моделируете быстроходный катер (который является транспортным средством) и утверждаете, что он показывает, что все машины быстрые и движутся по воде.

0 голосов
/ 02 ноября 2018

Дело в том, что мы можем реализовать все, что захотим, в структуре Functor, и в этом случае я просто сделал это IO / console.log значением.

Другое дело, что делать это монады совершенно не нужно.

Проблема в том, что, как только вы это сделаете, ваш функтор перестанет быть функтором. Функторы должны сохранять тождества и состав. Для Haskell Functor s эти требования составляют:

fmap id = id
fmap (g . f) = fmap g . fmap f

Эти законы являются гарантией того, что все, что делает fmap, использует предоставленную функцию для изменения значений - она ​​не делает смешных вещей за вашей спиной. В случае вашего кода fmap(x => x) ничего не должен делать; вместо этого он печатает на консоль.

Обратите внимание, что все вышеперечисленное относится к функтору IO: если a является действием IO, выполнение fmap f a не будет иметь никаких эффектов ввода / вывода, кроме тех, которые уже были a. Один удар по написанию чего-то похожего по духу на ваш код может быть ...

applyAndPrint :: Show b => (a -> b) -> a -> IO b
applyAndPrint f x = let y = f x in fmap (const y) (print y)

pseudoFmap :: Show b => (a -> b) -> IO a -> IO b
pseudoFmap f a = a >>= applyAndPrint f

... но уже используется Monad, так как у нас есть эффект (вывод результата), который зависит от результата предыдущего вычисления.

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

0 голосов
/ 02 ноября 2018

Есть ли контрпример, что Функторы не могут делать то, что могут делать Монады, кроме надежности функциональной композиции путем выравнивания вложенной структуры?

Я думаю, что это важный момент:

Monads - это небольшое расширение для надежности композиции функций путем объединения: M(M(X)) -> M(X), чтобы избежать вложенного типа.

«Избегать вложенного типа» на самом деле не цель join, это просто аккуратный побочный эффект. То, как вы это делаете, звучит так, будто join просто удаляет внешний тип, но значение монады не изменяется. Это не вариант. join является сердцем монады, и именно это позволяет монаде делать вещи .

Вы можете думать о функторе как о контейнере. Есть произвольный внутренний тип, и вокруг него внешняя структура, которая позволяет некоторой дисперсии, некоторым дополнительным значениям «украшать» ваше внутреннее значение. fmap позволяет вам работать с вещами внутри контейнера, так же, как вы обычно работаете с ними. Это в основном предел того, что вы можете сделать с функтором.

Монада - это функтор с особой силой: где fmap позволяет вам работать с внутренним значением, bind позволяет вам комбинировать внешние значения согласованным образом. Это гораздо мощнее простого функтора.


Дело в том, что мы можем реализовать все, что захотим, в структуре Functor, и в этом случае я просто сделал это IO / console.log значением.

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

Это грубое обобщение, но по большей части полезно описать IO как прославленную State монаду. Каждое действие IO принимает дополнительный скрытый параметр, называемый RealWorld (который представляет состояние реального мира), может считывать его или изменять его, а затем отправляет его на следующее действие IO. Этот параметр RealWorld пронизывает цепочку. Если что-то записывается на экран, это RealWorld копируется, модифицируется и передается. Но как работает «прохождение»? Ответ join.

Скажем, мы хотим прочитать строку от пользователя и вывести ее обратно на экран:

getLine :: IO String
putStrLn :: String -> IO ()

main :: IO ()
main = -- ?

Предположим, IO - функтор. Как мы это реализуем?

main :: IO (IO ())
main = fmap putStrLn getLine

Здесь мы подняли putStrLn до IO, чтобы получить fmap putStrLn :: IO String -> IO (IO ()). Если вы помните, putStrLn принимает String и скрытый RealWorld и возвращает измененный RealWorld, где параметр String выводится на экран. Мы подняли эту функцию с fmap, так что теперь она принимает IO (это действие, которое принимает скрытое RealWorld, возвращает измененные RealWorld и String) и возвращает то же самое действие io , только что обернутое вокруг другого значения (совершенно отдельное действие, которое также принимает отдельное скрытое RealWorld и возвращает RealWorld). Даже после применения getLine к этой функции на самом деле ничего не происходит и не печатается.

Теперь у нас есть main :: IO (IO ()). Это действие, которое принимает скрытое значение RealWorld и возвращает измененное значение RealWorld и отдельное действие. Это второе действие принимает другой RealWorld и возвращает другой измененный RealWorld. Само по себе это бессмысленно, ничего вам не приносит и ничего не выводит на экран. Что должно произойти, так это то, что два IO действия должны быть соединены вместе, так что возвращаемое одно действие RealWorld вводится как параметр RealWorld другого действия. Таким образом, это становится одной непрерывной цепочкой RealWorld s, которая мутирует с течением времени. Это «соединение» или «сцепление» происходит, когда два действия IO объединены с join.


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


РЕДАКТИРОВАТЬ: Ответы на комментарии и отредактированный вопрос:

Я не вижу никакого определения Монад в теории категорий, объясняющего это. Все, что я прочитал о соединении, это стиль MMX => MX, и это именно то, что делает мой код.

Вы также можете точно сказать, что делает функция String -> String? Может ли он не возвращать входные данные дословно, инвертировать его, фильтровать, добавлять к нему, игнорировать и возвращать совершенно другое значение или что-либо еще, что приводит к String? Тип не определяет, что функция делает, он ограничивает , что функция может делать. Поскольку join в общем случае определяется только его типом, любая конкретная монада может делать все, что разрешено этим типом . Это может быть просто удаление внешнего слоя, или это может быть какой-то чрезвычайно сложный метод объединения двух слоев в один. Пока вы начинаете с двух слоев и заканчиваете одним слоем, это не имеет значения. Тип допускает ряд возможностей, которые являются частью того, что делает монады такими мощными для начала.

В Хаскеле есть MaybeFunctor. Там нет «присоединиться» или «связать» там, и мне интересно, откуда приходит сила. В чем разница между MaybeFunctor и MaybeMonad?

Каждая монада также является функтором: монада является не чем иным, как функтором, который также имеет функцию join. Если вы используете join или bind с Maybe, вы используете его как монаду, и она обладает всей мощью монады. Если вы не используете join или bind, но используете только fmap и pure, вы используете его как функтор, и он становится ограниченным действиями, которые может делать функтор. Если нет join или bind, то нет дополнительной мощи монады.

Я полагаю, что «избегание вложенного типа» - это не просто аккуратный побочный эффект, но определение «соединения» Монады в теории категорий

Определение join - это преобразование из вложенной монады в не вложенную монаду. Опять же, это может означать что угодно . Сказать, что цель join - «избежать вложенного типа», все равно, что сказать, что цель + - избежать пар чисел. Большинство операций каким-то образом объединяют вещи, но очень немногие из этих операций существуют просто ради того, чтобы иметь комбинацию вещей. Важно как происходит объединение .

в Haskell есть Возможно, функтор , у которого нет join, или есть Свободная монада , которая join встроена с первого места в определенную структуру. Это объекты, которые пользователи определяют Функторы для делают вещи .

Я уже обсуждал Maybe, и как, когда вы используете его только как функтор, он не может делать то, что он может делать, если вы используете его как монаду. Free странно, потому что это одна из немногих монад, которые на самом деле ничего не делают .

Free можно использовать для превращения любого функтора в монаду, что позволяет использовать нотацию do и другие удобства. Однако самонадеянность Free состоит в том, что join не объединяет ваши действия так, как это делают другие монады, вместо этого он сохраняет их отдельно, вставляя их в структуру, похожую на список; идея состоит в том, что эта структура позднее обрабатывается, а действия объединяются отдельным кодом 1170 *. Эквивалентный подход состоял бы в том, чтобы переместить этот код обработки в саму join, но это превратило бы функтор в монаду, и не было бы никакого смысла в использовании Free. Таким образом, единственная причина, по которой Free работает, заключается в том, что она делегирует действительную часть "делания" части монады в другом месте; его join предпочитает отложить действие для кода, работающего вне монады. Это похоже на оператор +, который вместо добавления чисел возвращает абстрактное синтаксическое дерево; затем можно было обработать это дерево позже любым необходимым способом.

Эти наблюдения не соответствуют факту существования функтора Maybe и Свободной монады.

Вы не правы. Как объяснено, Maybe и Free отлично вписываются в мои предыдущие наблюдения:

  • Функтор Maybe просто не обладает той же выразительностью, что и монада Maybe.
  • Монада Free преобразует функторы в монады единственным возможным способом: не реализуя монадическое поведение, а вместо этого просто откладывая его до некоторого предполагаемого кода обработки.
0 голосов
/ 02 ноября 2018

Ваш «функтор», очевидно, не является функтором, нарушая и закон об идентификации, и состав:

console.log("Functor");
{
  const unit = (val) => ({
    // contextValue: () => val,
    fmap: (f) => unit((() => {
      //you can do pretty much anything here
      const newVal = f(val);
      console.log(newVal); //IO in the functional context
      return newVal;
    })()),
  });

  console.log("fmap(id) ...");
  const a0 = unit(3)
    .fmap(x => x);      // prints something
  console.log("         ≡ id?");
  const a1 = (x => x)(unit(3));   // prints nothing

  console.log("fmap(f) ∘ fmap(g) ...");
  const b0 = unit(3)
    .fmap(x => 3*x)
    .fmap(x => 4+x);     // prints twice
  console.log("                   ≡ fmap(f∘g)?");
  const b1 = unit(3)
    .fmap(x => 4+(3*x));    // prints once
}
...