3

假设我有一个这样定义的类型。

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

但是我不需要支持乘法和除法吗?将秒除以秒没有任何意义。我将如何使类型支持加法和减法?或者,如果这是不可能的,或者我的推理完全错误,那么在 Haskell 中这样做的惯用方式是什么?

4

4 回答 4

6

您对标准前奏不走运,Num 类型类要求您实现对这种数据类型没有意义的功能。基本上有三种选择

  1. 将函数命名为 + 和 - 以外的名称。这可能是首选选项。
  2. 实现 Num 类型类,但让没有意义的函数抛出错误。这样做的缺点是它将应该是编译时错误变成了运行时错误。
  3. 使用不同的 Prelude,例如将函数从 Num 拆分为其他类型类的Numeric Prelude 。这个选项在数学上是最正确的,但也有点不方便,因为它不使用标准的 Prelude。
于 2013-07-16T17:30:37.237 回答
3

首先——为什么不使用现有的物理量图书馆?例如,维度-tf。将自己限制为秒有点奇怪,因为这些实际上只是许多可能的时间单位之一,尽管您使用Integer而不是更明显的事实Double表明您确实对固定时间光栅感兴趣,量化为秒。

可以加减但不能相乘的东西的精确类型类存在:AdditiveGroupvector-spaces包中。

instance AdditiveGroup Seconds where
  zeroV = Seconds 0
  Seconds a ^+^ Seconds b = Seconds $ a+b
  negateV (Seconds a) = Seconds $ negate a

事实上,你也可以定义一个向量空间实例:

instance VectorSpace Seconds where
  type Scalar Seconds = Integer
  μ *^ (Seconds a) = Seconds $ μ * a

尽管这对于整数量化似乎并没有那么有用,但您通常会使用它type Scalar Seconds = Double

于 2013-07-17T10:05:08.347 回答
2

You could make functions named + and - that work on seconds, but there's no way for it to be the same + and - from the Num type class without making Seconds an instance of Num (which therefore will lead any code that gets a Seconds value as a generic Num a to expect that it can use the other Num functions as well).

All you have to do is explicitly import the Prelude, either hiding + and - or importing it qualified.

The trouble then is that any code using your + and - also has to do something to resolve the ambiguity with the Prelude + and -; either you only have one version of + in scope, or at least one of them must always be referred to with a qualified name (some variant of Prelude.+, P.+, S.+, Seconds.+, etc). For an obscure name, this is sometimes acceptable. It's probably not a good idea with something as common and fundamental as +.

You could make that option nicer by making + and - functions in a new type class (say PlusMinus), and write instance Num a => PlusMinus a where (+) = (Prelude.+) etc. You then also make Seconds an instance of PlusMinus.1

What this buys you is that any code that wants to use your new + operator can at least safely hide the Prelude's + while still being able to use + on other Num types. It does still impose some bother on every module wanting to use your + though, and it has the potential to be confusing (someone one day may see + being used on Seconds without being deeply familiar with all this, and assume that they can use other numeric operations on Seconds).

Probably better would be to make functions that aren't called + and -. You can use new multi-character operators containing + and - if you want (though it can be tricky to find ones that aren't used by other libraries).


Here's an approach I once took that was sort-of massive overkill, but also sort-of satisfying.

The problem was that I had vectors representing absolute positions, and also vectors representing offsets. I decided it made sense to add and subtract offsets, but not positions. However it did make sense to add an offset to a position to get a position, or to subtract two positions to get an offset, and even to multiply an offset by a scalar to get an offset.

So what I ended up doing was to define a type class something like this:

{-# 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

etc

So you end up using |+| rather than +, but it still ends up looking a bit like the algebra you're used to thinking in (once you get used to the convention that |+| is the "generalised" version of +, etc), and it lets you encode a lot of rules about what operations make sense in the type system, so the compiler can check them for you. The downside is a lot of boilerplate defining all the instances, but for a small fixed number of types that's something you only have to do once.


1You'll need extensions to make this work; it's a little unsafe in principle because there could be an instance of Num for Seconds out there somewhere, which would make Seconds match PlusMinus two different ways.

于 2013-07-17T08:20:34.350 回答
1

如果您想限制对数据类型的操作,标准技巧是不为您的类型导出构造函数。这样函数就无法访问数据的内部,只能使用您提供的操作。所以你想要这样的东西:

module Seconds (Seconds) where

newtype Seconds = Seconds Integer

mkSeconds  :: Integer -> Seconds
addSeconds :: Seconds -> Seconds -> Seconds
subSeconds :: Seconds -> Seconds -> Seconds

请注意,模块 export Seconds, not Seconds(..),因此类型 Seconds可用,但构造函数不可用。现在不可能写一个函数

dangerousMult :: Seconds -> Seconds -> Seconds
dangerousMult (Seconds i) (Seconds j) = Seconds (i * j)
于 2013-07-16T17:24:06.800 回答