Haskell: дублированные функции (+) и (++), mappend

(+) и (++) являются только специализациями mappend; я прав? Зачем они нужны? Это бесполезное дублирование, так как у Haskell есть эти мощные классы и тип вывода. Пусть, скажем, мы удалим (+) и (++) и переименуем mappend (+) для удобства просмотра и усиления ввода. Кодирование было бы более интуитивным, короче и понятным для новичков:

--old and new
1 + 2
--result
3

--old
"Hello" ++ " " ++ "World"
--new
"Hello" + " " + "World"
--result
"Hello World"

--old
Just [1, 2, 3] `mappend` Just [4..6]
--new
Just [1, 2, 3] + Just [4..6]
--result
Just [1, 2, 3, 4, 5, 6]

(Это заставляет меня мечтать.). Три и, возможно, больше, функции для одного и того же, не очень хорошо для красивого языка, который настаивает на абстракции и таких вещах, как Haskell. Я также видел такие же повторения с монадами: fmap то же самое или почти, как map, (.), liftM, mapM, forM,... Я знаю, что существуют исторические причины для fmap, но как насчет моноидов? Планирует ли комиссия Haskell что-то об этом? Это сломало бы некоторые коды, но я слышал, хотя я не уверен, что есть входящая версия, которая будет иметь большие изменения, что является отличным событием. Это слишком жаль... По крайней мере, вилка доступна?

ИЗМЕНИТЬ В ответах, которые я читал, есть факт, что для чисел либо (*), либо (+) может поместиться в mappend. На самом деле, я думаю, что (*) должен быть частью Monoid! Посмотрите:

В настоящее время, сбрасывая функции mempty и mconcat, мы имеем только mappend.

class Monoid m where
    mappend :: m -> m -> m

Но мы могли бы это сделать:

class Monoid m where
    mappend :: m -> m -> m
    mmultiply :: m -> m -> m

Это могло бы (возможно, я пока еще не достаточно об этом) ведет себя следующим образом:

3 * 3
mempty + 3 + 3 + 3
0 + 3 + 3 + 3
9

Just 3 * Just 4
Just (3 * 4)
Just (3 + 3 + 3 +3)
Just 12

[1, 2, 3] * [10, 20, 30]
[1 * 10, 2 * 10, 3 * 10, ...]
[10, 20, 30, 20, 40, 60, ...]

Фактически "mmultiply" будет просто определен только в терминах "mappend", поэтому для экземпляров Monoid нет необходимости переопределять его! Тогда Monoid ближе к математике; возможно, мы могли бы добавить (-) и (/) в класс! Если это сработает, я думаю, что он разрешит случай Sum и Product, а также дублирование функций: mappend станет (+), а новый mmultiply - просто (*). В основном я предлагаю рефакторинг кода с "pull up". О, нам понадобится также новый mempty для (*). Мы могли бы абстрагировать эти операторы в классе MonoidOperator и определить Monoid следующим образом:

class (Monoid m) => MonoidOperator mo m where
    mempty :: m
    mappend :: m -> m -> m

instance MonoidOperator (+) m where
    mempty = 0
    mappend = --definition of (+)

instance MonoidOperator (*) where
    --...

class Monoid m where
    -...

Ну, я не знаю, как это сделать, но я думаю, что для всего этого есть классное решение.

Ответ 1

Здесь вы пытаетесь смешивать несколько отдельных понятий.

Арифметика и конкатенация списков - очень практичные, прямые операции. Если вы пишете:

[1, 2] ++ [3, 4]

... вы знаете, что в результате вы получите [1, 2, 3, 4].


A Monoid - математическое алгебраическое понятие, которое находится на более абстрактном уровне. Это означает, что mappend не означает, что буквально означает "добавить это к этому"; он может иметь много других значений. Когда вы пишете:

[1, 2] `mappend` [3, 4]

... это некоторые достоверные результаты, которые может произвести эта операция:

[1, 2, 3, 4] -- concatenation, mempty is []

[4, 6]       -- vector addition with truncation, mempty is [0,0..]

[3, 6, 4, 8] -- some inner product, mempty is [1]

[3, 4, 6, 8] -- the cartesian product, mempty is [1]

[3, 4, 1, 2] -- flipped concatenation, mempty is []

[]           -- treating lists like `Maybe a`, and letting lists that
             -- begin with positive numbers be `Just`s and other lists
             -- be `Nothing`s, mempty is []

Почему mappend для списков просто объединяет списки? Потому что это просто определение для моноидов, что ребята, которые написали Haskell Report, выбрали в качестве реализации по умолчанию, вероятно, потому, что это имеет смысл для всех типов элементов списка. И действительно, вы можете использовать альтернативный экземпляр Monoid для списков путем их переноса в различные типы newtypes; существует, например, альтернативный экземпляр Monoid для списков, которые выполняют на них декартовы произведения.

Понятие "моноид" имеет фиксированное значение и долгую историю в математике, и изменение его определения в Haskell означает отклонение от математической концепции, которой не должно быть. Моноид - это не просто описание пустого элемента и операции (добавление/конкатенация); это основа для широкого спектра концепций, которые соответствуют интерфейсу, который предоставляет Monoid.


Концепция, которую вы ищете, специфична для чисел (потому что вы не можете определить что-то вроде mmultiply или, возможно, mproduce/mproduct для всех экземпляров Maybe a, например), концепция, которая уже существует и называется Semiring в математике (ну, вы действительно не рассматривали ассоциативность в своем вопросе, но вы прыгаете между различными понятиями в ваши примеры в любом случае "иногда придерживаются ассоциативности, иногда нет", но общая идея такая же).

В Haskell уже реализованы реализации Semirings, например, в пакете algebra.

Однако моноид не является, как правило, Semiring, а также есть несколько реализаций Semirings для действительных чисел, кроме, в частности, добавления и умножения. Добавление широких обобщенных дополнений к очень четким типам классов, таких как Monoid, не должно выполняться только потому, что оно "было бы аккуратным" или "сэкономило бы несколько нажатий клавиш"; есть причина, по которой мы имеем (++), (+) и mappend как отдельные понятия, потому что они представляют собой совершенно разные вычислительные идеи.

Ответ 2

При переименовании mappend на (+)/(*)

В то время как (+) и (*) являются моноидами, они имеют дополнительные законы распределения, относящиеся к двум операциям, а также законы отмены, например. 0 * x = 0. По существу, (+) и (*) образуют ring. Две моноиды на каком-то другом типе могут не удовлетворять этим кольцам (или даже более слабым полукольцевым) свойствам. Именование операторов (+) и (*) указывает на их дополнительные (взаимосвязанные) свойства. Таким образом, я бы избегал подрывать традиционные математические интуиции, переименовывая mappend в + или *, поскольку эти имена предполагают дополнительные свойства, которые могут не выполняться. Иногда слишком много перегрузки (т.е. Слишком много обобщений) приводит к потере интуиции и, следовательно, к снижению удобства использования.

Если у вас есть две моноиды, которые образуют какое-то кольцо, вам может понадобиться извлечь из него экземпляр Num, так как имена "+" и "*" предполагают дополнительные свойства.

В conflating (++) и mappend

Переименование mappend в (++) может быть более уместным, так как с помощью (++) существует меньший дополнительный ментальный багаж. Действительно, поскольку списки являются свободным моноидом (то есть моноидом без дополнительных свойств), то использование традиционного оператора списка-конкатенации (++) для обозначения бинарной операции моноида не кажется страшной идеей.

При определении нескольких моноидов для одного типа

Как вы указываете, обе (+) являются (*) являются моноидами, но оба нельзя сделать экземпляром Monoid для того же типа t. Одним из решений, которое вы получаете наполовину, является наличие дополнительного параметра типа для класса Monoid для выделения двух моноидов. Обратите внимание, что классы типов могут быть параметризированы только типами, а не выражением, которое вы укажете в своем вопросе. Подходящим определением будет что-то вроде:

class Monoid m variant where
 mappend :: variant -> m -> m -> m
 mempty :: variant -> m

data Plus = Plus
data Times = Times

instance Monoid Int Plus where
    mappend Plus x y = x `intPlus` y
    mempty = 0

instance Monoid Int Times where
    mappend Times x y = x `intTimes` y
    mempty = 1

(+) = mappend Plus
(*) = mappend Times 

Чтобы приложения mappend/mempty были разрешены для конкретной операции, каждый должен принять значение, свидетельствующее тип, указывающий конкретный моноидный "вариант".

Кроме того, в названии вашего вопроса упоминается mconcat. Это совершенно другая операция с mappend - mconcat является моноидным гомоморфизмом из свободного моноида в какой-либо другой моноид, т.е. Заменяет cons на mappend и nil на mempty.

Ответ 4

Ну есть две моноиды для чисел - Product и Sum, как бы вы справились с этим?

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

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