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,我们可以为a、b 和c 类型的组合声明一个类型类,如Mul。参数之后的部分,| 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)
现在,如果我们不想定义新的操作数,而是希望从 Prelude 覆盖默认的 Num 类,我们可以这样做
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)