Каково практическое применение моноидов?

Я читаю Learn You a Haskell и уже рассмотрел аппликатив, и теперь я на моноидах. У меня нет проблем с пониманием обоих, хотя я нашел аппликатив полезным на практике, а моноид - не совсем так. Поэтому я думаю, что я чего-то не понимаю в Haskell.

Во-первых, говоря о Applicative, он создает что-то вроде единого синтаксиса для выполнения различных действий над «контейнерами». Таким образом, мы можем использовать обычные функции для выполнения действий над Maybe, списками, IO (должен ли я был сказать монады? Я еще не знаю монады), функциями:

λ> :m + Control.Applicative
λ> (+) <$> (Just 10) <*> (Just 13)
Just 23
λ> (+) <$> [1..5] <*> [1..5]
[2,3,4,5,6,3,4,5,6,7,4,5,6,7,8,5,6,7,8,9,6,7,8,9,10]
λ> (++) <$> getLine <*> getLine
one line
 and another one
"one line and another one"
λ> (+) <$> (* 7) <*> (+ 7) $ 10
87

Итак, аппликатив — это абстракция. Я думаю, что мы можем жить без него, но он помогает четко выражать некоторые идеи, и это нормально.

Теперь давайте посмотрим на Monoid. Это тоже абстракция и довольно простая. Но поможет ли это нам? Для каждого примера из книги кажется очевидным, что есть более понятный способ сделать что-то:

λ> :m + Data.Monoid
λ> mempty :: [a]
[]
λ> [1..3] `mappend` [4..6]
[1,2,3,4,5,6]
λ> [1..3] ++ [4..6]
[1,2,3,4,5,6]
λ> mconcat [[1,2],[3,6],[9]]
[1,2,3,6,9]
λ> concat [[1,2],[3,6],[9]]
[1,2,3,6,9]
λ> getProduct $ Product 3 `mappend` Product 9
27
λ> 3 * 9
27
λ> getProduct $ Product 3 `mappend` Product 4 `mappend` Product 2
24
λ> product [3,4,2]
24
λ> getSum . mconcat . map Sum $ [1,2,3]
6
λ> sum [1..3]
6
λ> getAny . mconcat . map Any $ [False, False, False, True]
True
λ> or [False, False, False, True]
True
λ> getAll . mconcat . map All $ [True, True, True]
True
λ> and [True, True, True]
True

Итак, мы заметили некоторые закономерности и создали новый класс шрифтов... Хорошо, я люблю математику. Но с практической точки зрения, какой смысл в Monoid? Как это помогает нам лучше выражать идеи?


person Mark Karpov    schedule 07.10.2014    source источник
comment
Если вы хотите покопаться в каком-нибудь продвинутом материале, который действительно демонстрирует мощь моноидов, посмотрите деревья пальцев (движок позади Data.Sequence). Я уверен, что есть более простые примеры, которые я не могу сейчас придумать.   -  person luqui    schedule 07.10.2014
comment
Я бы посоветовал вам пройти через этот превосходный Дэн Пипони< /а> статья.   -  person Sibi    schedule 07.10.2014
comment
Также я бы посоветовал прочитать Monoids and Finger Trees, где показано, как мы может использовать один и тот же код для работы с деревьями и реализовывать разные структуры данных, просто подключая разные моноиды.   -  person Petr    schedule 08.10.2014


Ответы (4)


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

Речь идет о масштабируемости, архитектуре и дизайне API. Идея в том, что есть «Традиционная архитектура», которая гласит:

Объедините вместе несколько компонентов типа A, чтобы создать «сеть» или «топологию» типа B.

Проблема с таким дизайном заключается в том, что по мере масштабирования вашей программы растет и ваш ад при рефакторинге.

Итак, вы хотите изменить модуль А, чтобы улучшить свой дизайн или домен, и вы это делаете. О, но теперь сломались модули B и C, зависящие от A. Вы исправляете B, отлично. Теперь вы исправляете C. Теперь B снова сломался, так как B также использовал некоторые функции C. И я могу продолжать это вечно, и если вы когда-либо использовали ООП, то и вы сможете.

Затем есть то, что Габриэль называет «архитектурой Haskell»:

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

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

Так что теперь поощряется то, что вместо того, чтобы говорить что-то вроде «У меня есть несколько X, поэтому давайте создадим тип, представляющий их объединение», вы говорите: «У меня есть несколько X, поэтому давайте объединим их в X». Или на простом английском: «Давайте в первую очередь сделаем составные типы». (ты уже чувствуешь, что моноиды прячутся?).

Представьте, что вы хотите создать форму для своей веб-страницы или приложения, и у вас есть модуль «Форма личной информации», который вы создали, потому что вам нужна была личная информация. Позже вы обнаружили, что вам также нужно «Изменить форму изображения», поэтому быстро написали это. А теперь вы говорите, что я хочу их объединить, поэтому давайте сделаем модуль «Личная информация и форма изображения». И в реальных масштабируемых приложениях это может выйти из-под контроля. Вероятно, не с формами, а для демонстрации, вам нужно сочинять и сочинять, чтобы в итоге вы получили «Личная информация и изменить изображение и изменить пароль и изменить статус и управлять друзьями и управлять списком желаний и изменять настройки просмотра и, пожалуйста, не расширяйте меня больше И пожалуйста, пожалуйста, остановитесь! И СТОП!!!!" модуль. Это некрасиво, и вам придется управлять этой сложностью в API. О, и если вы хотите что-то изменить - у него, вероятно, есть зависимости. Итак.. да.. Добро пожаловать в ад.

Теперь давайте посмотрим на другой вариант, но сначала давайте посмотрим на выгоду, потому что она приведет нас к ней:

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

Звучит хорошо, так что вместо того, чтобы создавать модуль «Форма личной информации» / «Форма изменения изображения», остановитесь и подумайте, можем ли мы сделать что-нибудь здесь составным. Ну, мы можем просто сделать "Форму", верно? был бы также более абстрактным.
Тогда может иметь смысл создать один для всего, что вы хотите, объединить их вместе и получить одну форму, такую ​​же, как и любую другую.

Итак, вы больше не получаете запутанное сложное дерево из-за ключа, что вы принимаете две формы и получаете одну форму. Таким образом, 1_. И, как вы уже ясно видите, эта подпись является экземпляром mappend.

Альтернативная и обычная архитектура, вероятно, выглядели бы как a -> b -> c, затем c -> d -> e, а затем...

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

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

person MasterMastic    schedule 07.10.2014
comment
Вы также можете упомянуть эту публикацию, которая конкретно использует Monoid в качестве рабочего примера. - person Gabriel Gonzalez; 07.10.2014

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

Это API. Простой. Для типов, которые поддерживают:

  • имеющий нулевой элемент
  • наличие операции добавления

Многие типы поддерживают эти операции. Таким образом, наличие имени для операций и API помогает нам более четко уловить факт.

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

person Don Stewart    schedule 07.10.2014

Дело в том, что когда вы помечаете Int как Product, вы выражаете свое намерение умножать целые числа. И пометив их как Sum, они будут добавлены вместе.

Затем вы можете использовать один и тот же mconcat на обоих. Это используется, например. в Foldable, где один foldMap выражает идею сворачивания содержащей структуры, при этом объединяя элементы определенным способом моноида.

person Will Ness    schedule 07.10.2014

Очень простой пример: foldMap. Просто подключив разные моноиды к этой единственной функции, вы можете вычислить:

  • первый и < элемент href="http://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Monoid.html#t:Last" rel="noreferrer">последний элемент,
  • сумма или < href="http://hackage.haskell.org/package/base-4.7.0.1/docs/Data-Monoid.html#t:Product" rel="noreferrer">произведение элементов (из этого также их средние и т. д.),
  • проверьте, есть ли все элементы или любой имеет заданное свойство ,
  • вычислить максимальный или минимальный элемент,
  • сопоставить элементы коллекции (например, списки, наборы, строки, текст, ByteString или ByteString Builder ) и соедините их вместе — все они моноиды.

Более того, моноиды составны: если a и b моноиды, то и (a, b) тоже. Таким образом, вы можете легко вычислить несколько разных моноидальных значений за один проход (например, сумму и произведение при вычислении среднего значения элементов и т. д.).

И хотя все это можно сделать без моноидов, используя foldr или foldl, это гораздо более громоздко, а также часто менее эффективно: например, если у вас есть сбалансированное бинарное дерево и вы хотите найти его минимальный и максимальный элемент, вы не можете сделать оба эффективно с foldr (или оба с foldl), всегда будет O(n) для одного из случаев, а при использовании foldMap с соответствующими моноидами это будет O( log n) в обоих случаях.

И все это была всего лишь одна функция foldMap! Есть много других интересных приложений. Например, возведение в степень путем возведения в квадрат – эффективный способ увеличения вычислительной мощности. Но на самом деле это не связано с вычислительными мощностями. Вы можете реализовать его для любого моноида, и если его <> равно O(1), у вас есть эффективный способ вычисления n раз x <> ... <> x. И вдруг вы можете сделать эффективное возведение матрицы в степень и вычислить n-th число Фибоначчи всего за O(log n) умножений. См. times1p в полугруппа.

См. также Моноиды и деревья пальцев.

person Petr    schedule 08.10.2014