Числовые операции в классе типов 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)