Haskell类型仅支持加法和减法

时间:2013-07-16 17:17:58

标签: haskell time timer

假设我有一个类似的类型。

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中这样做的惯用方法是什么?

4 个答案:

答案 0 :(得分:6)

你对标准前奏感到不满意,Num类型类需要你实现对这种数据类型没有意义的函数。基本上有三个选项

  1. 将函数命名为+和 - 以外的函数。这可能是首选方案。
  2. 实现Num类型类,但具有无意义的函数会抛出错误。这有一个缺点,它将应该是编译时错误变成运行时错误。
  3. 使用不同的Prelude,例如将函数从Num分割为其他类型类的Numeric Prelude。此选项在数学上是最正确的,但也有点不方便,因为它不使用标准Prelude。

答案 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上看到+而没有被深深地使用熟悉所有这些,并假设他们可以在+上使用其他数字操作。

可能更好的是制作未被称为SecondsSeconds的函数。如果需要,可以使用包含+-的新多字符运算符(尽管查找其他库未使用的运算符可能很棘手)。


这是我曾经采取的一种方法,这种做法有点过分,但也很令人满意。

问题在于我有表示绝对位置的向量,以及表示偏移的向量。我认为增加和减去偏移是有意义的,但不是位置。然而,在位置上添加偏移量以获得位置,或者减去两个位置以获得偏移量,甚至将偏移量乘以标量以获得偏移量确实有意义。

所以我最终做的是定义类似这样的类型:

+

所以你最终使用-而不是{-# 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)