Пользовательские математические типы и классы на Haskell - PullRequest
4 голосов
/ 22 февраля 2012

Есть ли способ заставить экземпляр класса возвращать значение, которое не относится к типу экземпляра? Примером является желание вернуть значение типа Double для скалярного произведения двух векторов:

--  data structure to contain a 3D point in space
data Point3D = Point3D !Double !Double !Double
    deriving (Eq, Ord)

instance Num Point3D where
    -- Multiplication, scalar == Dot product
    Point3D x1 y1 z1 * Point3D x2 y2 z2 = x1*x2 + y1*y2 + z1*z2 :: Double

Кроме того, есть ли способ определить, как операторы работают между функциями разных типов? Например, я хотел бы определить Point3D x y z + Double a = Point3D (x + a) (y + a) (z + a)

Ответы [ 3 ]

6 голосов
/ 22 февраля 2012

Числовые операции в классе типов Num все определены с типом :: Num n => n -> n -> n, поэтому оба операнда и возвращаемое значение должны иметь одинаковый тип. Нет никакого способа изменить существующий класс типов, поэтому вы можете либо определить новые операторы, либо скрыть существующий класс Num и полностью заменить его собственной реализацией.

Чтобы реализовать операторы, которые могут иметь разные типы операндов, вам понадобится пара языковых расширений.

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}

Вместо Num -подобного класса, который включает в себя +, - и *, более гибко определить различные классы типов для разных операндов, потому что, хотя Point3D * Double имеет смысл, Point3D + Double обычно делает не. Давайте начнем с Mul.

class Mul a b c | a b -> c where
    (|*|) :: a -> b -> c

Без расширений классы типов всегда содержат только один параметр типа, но с помощью MultiParamTypeClasses мы можем объявить класс типов как Mul для комбинации типов a, b и c. Часть после параметров | a b -> c является «функциональной зависимостью», которая в этом случае утверждает, что тип c зависит от a и b. Это означает, что если у нас есть экземпляр типа Mul Double Point3D Point3D, то функциональная зависимость заявляет, что у нас не может быть других экземпляров Mul Double Point3D c, где c что-то отличное от Point3D, то есть тип возврата умножения всегда однозначно определяется типом операндов.

Вот как мы реализуем экземпляры для Mul:

instance Mul Double Double Double where
    (|*|) = (*)

instance Mul Point3D Double Point3D where
    Point3D x y z |*| a = Point3D (x*a) (y*a) (z*a)

instance Mul Double Point3D Point3D where
    a |*| Point3D x y z = Point3D (x*a) (y*a) (z*a)

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

p = Point3D 1 2 3 |*| 5

Поскольку литерал 5 не обязательно имеет тип Double. Это может быть любой Num n => n, и вполне возможно, что кто-то объявит новые экземпляры, такие как Mul Point3D Int Int, которые ведут себя совершенно иначе. Так что это означает, что нам нужно явно указывать типы числовых литералов.

p = Point3D 1 2 3 |*| (5 :: Double)

Теперь, если вместо определения новых операндов мы хотим переопределить класс Num по умолчанию из Prelude, мы можем сделать это следующим образом

import Prelude hiding (Num(..))
import qualified Prelude as P

class Mul a b c | a b -> c where
    (*) :: a -> b -> c

instance Mul Double Double Double where
    (*) = (P.*)

instance Mul Point3D Double Point3D where
    Point3D x y z * a = Point3D (x*a) (y*a) (z*a)
5 голосов
/ 22 февраля 2012

Нет способа заставить стандартные функции Num (включая операторы) возвращать другой тип.* имеет тип Num n => n -> n -> n, что означает, что n должен быть одинаковым во всем.

Также нет способа заставить стандартную функцию Num (например, +) работать саргументы двух разных типов.

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

Если вы не против Unicode, вы можете использовать · для своего точечного продукта :).Haskell поддерживает это, но другие программы могут испытывать затруднения при наборе юникода.

4 голосов
/ 22 февраля 2012

Вы можете создать свой пользовательский класс с умножением, которое может принимать разные типы.

import Prelude hiding ((*))
import qualified Prelude

class Mul a b c | a b -> c where (*) :: a -> b -> c
instance Mul Double Double Double where (*) = (Prelude.*)
instance Mul Double Int Double where a * b = a Prelude.* fromIntegral b
...

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

...