Подтвердить что ты не робот

Можете ли вы перегрузить + в haskell?

В то время как я видел всевозможные странные вещи в примере кода Haskell - я никогда не видел, чтобы оператор был перегружен. Есть ли что-то особенное?

Скажем, у меня такой тип, как Pair, и я хочу иметь что-то вроде

 Pair(2,4) + Pair(1,2) = Pair(3,6)

Можно ли сделать это в haskell?

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

4b9b3361

Ответ 1

Да

(+) является частью класса Num, и кажется, что вы не можете определить (*) и т.д. для своего типа, но я категорически не согласен.

newtype Pair a b = Pair (a,b)  deriving (Eq,Show) 

Я думаю, что Pair a b будет приятнее, или мы могли бы просто использовать тип (a,b) напрямую, но...

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

instance (Num a,Num b) => Num (Pair a b) where
   Pair (a,b) + Pair (c,d) = Pair (a+c,b+d)
   Pair (a,b) * Pair (c,d) = Pair (a*c,b*d)
   Pair (a,b) - Pair (c,d) = Pair (a-c,b-d)
   abs    (Pair (a,b)) = Pair (abs a,    abs b) 
   signum (Pair (a,b)) = Pair (signum a, signum b) 
   fromInteger i = Pair (fromInteger i, fromInteger i)

Теперь мы явно перегрузили (+), но также перешли весь hog и перегрузили (*) и все остальные функции Num тем же, очевидным, знакомым способом, что математика делает это для пары. Я просто не вижу проблемы с этим. На самом деле я считаю это хорошей практикой.

*Main> Pair (3,4.0) + Pair (7, 10.5)
Pair (10,14.5)
*Main> Pair (3,4.0) + 1    -- *
Pair (4,5.0)

* - Обратите внимание, что fromInteger применяется к числовым литералам типа 1, поэтому в этом контексте это было интерпретировано как Pair (1,1.0) :: Pair Integer Double. Это также очень приятно и удобно.

Ответ 2

Перегрузка в Haskell доступна только с использованием классов типов. В этом случае (+) принадлежит классу типа Num, поэтому вам нужно предоставить экземпляр Num для вашего типа.

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

Поэтому, если это не так, я бы рекомендовал вместо этого определить новый оператор. Например,

data Pair a b = Pair a b
    deriving Show

infixl 6 |+| -- optional; set same precedence and associativity as +
Pair a b |+| Pair c d = Pair (a+c) (b+d)

Затем вы можете использовать его, как и любой другой оператор:

> Pair 2 4 |+| Pair 1 2
Pair 3 6

Ответ 3

Я постараюсь ответить на этот вопрос очень напрямую, так как вы заинтересованы в том, чтобы получить "да" или "нет" при перегрузке (+). Ответ - да, вы можете перегрузить его. Есть два способа перегрузить его напрямую, без каких-либо изменений, и один из способов перегрузить его "правильно", что требует создания экземпляра Num для вашего типа данных. Правильный способ развивается в других ответах, поэтому я не буду его обсуждать.

Изменить: обратите внимание, что я не рекомендую описанный ниже способ, просто документируя его. Вы должны реализовать класс Num и не писать что-либо здесь.

Первым (и самым "неправильным" ) способом перегрузки (+) является просто скрыть функцию Prelude. + и определить вашу собственную функцию с именем (+), которая работает с вашим типом данных.

import Prelude hiding ((+)) -- hide the autoimport of +
import qualified Prelude as P -- allow us to refer to Prelude functions with a P prefix

data Pair a = Pair (a,a)

(+) :: Num a => Pair a -> Pair a -> Pair a -- redefinition of (+)
(Pair (a,b)) + (Pair (c,d)) = Pair ((P.+) a c,(P.+) b d ) -- using qualified (+) from Prelude

Здесь вы можете увидеть некоторые искажения, чтобы скрыть регулярное определение (+) от импорта, но нам все же нужен способ ссылаться на него, поскольку это единственный способ быстро выполнить добавление машины ( это примитивная операция).

Второй (немного менее ошибочный) способ сделать это - определить свой собственный класс, который включает только нового оператора, которого вы называете (+). Вам все равно придется скрывать старый (+), поэтому haskell не путается.

import Prelude hiding ((+))
import qualified Prelude as P

data Pair a = Pair (a,a)

class Addable a where
   (+) :: a -> a -> a

instance Num a => Addable (Pair a) where
    (Pair (a,b)) + (Pair (c,d)) = Pair ((P.+) a c,(P.+) b d )

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

Но ни один из них не рекомендуется, поскольку, как вы можете видеть, очень неудобно обращаться к регулярному (+) оператору, который определен в классе типов Num. Несмотря на то, что haskell позволяет вам переопределить (+), вся прелюдия и библиотеки ожидают оригинального (+) определения. К счастью для вас, (+) определяется в классе, поэтому вы можете просто сделать пар экземпляром Num. Вероятно, это лучший вариант, и это рекомендуют другие ответчики.

Проблема, с которой вы сталкиваетесь, заключается в том, что, возможно, слишком много функций определено в Num typeclass (+ является одним из них). Это всего лишь историческая катастрофа, и теперь использование Num настолько распространено, что его сейчас будет сложно изменить. Вместо того, чтобы разделить эти функциональные возможности на отдельные классы для каждой функции (чтобы их можно было переопределить отдельно), они все сближаются. В идеале Prelude будет иметь добавочное typeclass и Subtractable typeclass и т.д., Которые позволят вам определить экземпляр для одного оператора за раз, не выполняя все, что Num имеет в нем.

Как бы то ни было, факт заключается в том, что вы будете сражаться в гору, если хотите написать новый (+) только для вашего типа данных Pair. Слишком большая часть другого кода Haskell зависит от класса Num и его текущего определения.

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

Ответ 4

Перегрузка в Haskell стала возможной благодаря классам классов. Для хорошего обзора вы можете посмотреть в этом разделе в разделе "Узнайте о вас" Haskell.

Оператор (+) является частью класса типа Num из Prelude:

class (Eq a, Show a) => Num a where
  (+), (*), (-) :: a -> a -> a
  negate :: a -> a
  ...

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

Если у вас есть тип:

data Pair a = Pair (a, a) deriving (Show, Eq)

Тогда у вас может быть определение типа:

instance Num a => Num (Pair a) where
  Pair (x, y) + Pair (u, v) = Pair (x+u, y+v)
  ...

Пробивая это в ghci, мы получаем:

*Main> Pair (1, 2) + Pair (3, 4)
Pair (4,6)

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

Ответ 5

Если вам нужен только оператор (+), а не все операторы Num, возможно, у вас есть экземпляр Monoid, например, Monoid экземпляр пары выглядит так:

class (Monoid a, Monoid b) => Monoid (a, b) where
    mempty = (mempty, mempty)
    (a1, b1) `mappend` (a2, b2) = (a1 `mappend` a2, b1 `mappend` b2)

Вы можете сделать (++) псевдоним mappend, тогда вы можете написать код следующим образом:

(1,2) ++ (3,4) == (4,6)
("hel", "wor") ++ ("lo", "ld") == ("hello", "world")