假设我有一个类似的类型。
data Seconds | Seconds Integer
我可以像这样定义一个倒计时功能。
decrementTimer :: Seconds -> Seconds -> Seconds
decrementTimer (Seconds internalSecondsOne) (Seconds internalSecondsTwo) = Seconds $ internalSecondsOne - internalSecondsTwo
但这看起来既繁琐又凌乱,而且我必须为每一个时间代表做这件事;小时,分钟,持有秒,分钟和小时的时间段数据。
我真正想做的是“实现”(?)Num类型,所以我可以这样做。
decrementTimer :: Seconds -> Seconds -> Seconds
decrementTimer a b = a - b
但是我不需要支持乘法和除法吗?将Seconds除以Seconds并没有多大意义。我如何进行类型支持加法和减法?或者,如果这是不可能的,或者我的推理是完全错误的,那么在Haskell中这样做的惯用方法是什么?
答案 0 :(得分:6)
你对标准前奏感到不满意,Num类型类需要你实现对这种数据类型没有意义的函数。基本上有三个选项
答案 1 :(得分:3)
首先 - 为什么不使用现有的物理量库?例如,dimensional-tf。把你自己限制在几秒钟是很奇怪的,因为这些只是众多可能时间单位中的一个,尽管你使用Integer
而不是更明显Double
的事实表明你确实感兴趣一个固定的时间栅格,量化为秒。
AdditiveGroup
包中存在vector-spaces
instance AdditiveGroup Seconds where
zeroV = Seconds 0
Seconds a ^+^ Seconds b = Seconds $ a+b
negateV (Seconds a) = Seconds $ negate a
实际上,您还可以定义vector space实例:
instance VectorSpace Seconds where
type Scalar Seconds = Integer
μ *^ (Seconds a) = Seconds $ μ * a
虽然这对整数量化似乎没有用,但通常会改为type Scalar Seconds = Double
。
答案 2 :(得分:2)
你可以制作名为+
和-
的函数,这些函数可以在几秒钟内完成,但是没有办法使它成为相同的+
和{{1来自-
类型类而不使Num
成为Seconds
的实例(因此会将任何获得Num
值的代码作为通用Seconds
引导期望它也可以使用其他Num a
函数。
您所要做的就是明确导入Prelude,隐藏Num
和+
或导入合格。
问题在于,使用-
和+
的任何代码都必须采取行动来解决前奏-
和+
的歧义;您在范围内只有一个-
版本,或者必须始终使用限定名称引用其中一个版本(+
,Prelude.+
,{{1}的某种变体},P.+
等)。对于一个不起眼的名字,这有时是可以接受的。对于像S.+
这样普通和基本的东西,这可能不是一个好主意。
您可以通过在 new 类型类(例如Seconds.+
)中创建+
和+
函数来改善选项,并编写-
然后,您还可以PlusMinus
instance Num a => PlusMinus a where (+) = (Prelude.+)
的实例。 1
这样购买的是,任何想要使用新Seconds
运算符的代码都可以至少安全地隐藏Prelude的PlusMinus
,同时仍然能够在其他+
上使用+
1}}类型。它仍然会对每个想要使用+
的模块施加一些麻烦,但它有可能让人感到困惑(有一天某人可能会在Num
上看到+
而没有被深深地使用熟悉所有这些,并假设他们可以在+
上使用其他数字操作。
可能更好的是制作未被称为Seconds
和Seconds
的函数。如果需要,可以使用包含+
和-
的新多字符运算符(尽管查找其他库未使用的运算符可能很棘手)。
这是我曾经采取的一种方法,这种做法有点过分,但也很令人满意。
问题在于我有表示绝对位置的向量,以及表示偏移的向量。我认为增加和减去偏移是有意义的,但不是位置。然而,在位置上添加偏移量以获得位置,或者减去两个位置以获得偏移量,甚至将偏移量乘以标量以获得偏移量确实有意义。
所以我最终做的是定义类似这样的类型:
+
等
所以你最终使用-
而不是{-# LANGUAGE MultiParamTypeClasses, TypeFamilies #-}
class Addable a b where
type Result a
(|+|) :: a -> b -> Result a b
instance Addable Offset Offset where
type Result Offset Offset = Offset
o |+| o = ...
instance Addable Position Offset where
type Result Position Offset = Position
p |+| o = ...
instance Addable Offset Position where
type Result Offset Position = Position
o |+| p = p |+| o
,但它仍然看起来有点像你习惯的代数(一旦你习惯了{{{1} 1}}是|+|
等的“通用”版本,它允许您编码关于类型系统中哪些操作有意义的许多规则,因此编译器可以为您检查它们。缺点是定义了所有实例的很多样板,但对于少量固定数量的类型,你只需要做一次。
1 你需要扩展来完成这项工作;它原则上有点不安全,因为某处可能有+
|+|
的实例,这会使+
以两种不同的方式匹配Num
。
答案 3 :(得分:1)
如果要限制对数据类型的操作,标准技巧是不导出类型的构造函数。这样,函数无法访问数据的内部,并且只能使用您提供的操作。所以你想要的东西是:
module Seconds (Seconds) where
newtype Seconds = Seconds Integer
mkSeconds :: Integer -> Seconds
addSeconds :: Seconds -> Seconds -> Seconds
subSeconds :: Seconds -> Seconds -> Seconds
请注意,该模块会导出Seconds
,而不是Seconds(..)
,因此类型 Seconds
可用,但构造函数是不。现在写一个函数是不可能的
dangerousMult :: Seconds -> Seconds -> Seconds
dangerousMult (Seconds i) (Seconds j) = Seconds (i * j)