有没有办法限制ADT数据,以便只能构建有效数据?

时间:2016-10-27 15:07:04

标签: haskell functional-programming algebraic-data-types type-theory

当我想定义整数时,我尝试了这个

data Int = Pos Nat | Neg Nat
data Nat = Zero | Succ Nat

但在写下上述代码后,我发现可以构建Pos ZeroNeg Zero,这是不可取的。

当我想定义 Rational 时,再次出现同样的问题

data Rational = Rational Int Int 

我期待任何Rational n m,它应该满足m > 0 && gcd n m == 1

但我知道无法确保我的ADT数据满足这些属性。因此,当我编写一些将其作为输入处理的函数时,我必须考虑这些非法情况。 我希望有一些方法可以在定义ADT时定义一次属性,而不是每次使用时都检查属性

2 个答案:

答案 0 :(得分:4)

对于整数,这很容易:

data Nat = Zero | Positive Pos
data Pos = One | Succ Pos
data Int = Pos Nat | Neg Pos

或者您可以继续使用Int = Pos Nat | Neg Nat,但要解释Neg n代表-(n+1)而不是-n,因此“-0”将无法实现。

对于理性而言,它更棘手。我认为通常最好的做法是将这些非规范化值保留在实现中,但语义上只考虑这些值的等价类,或者使用智能构造函数对它们进行规范化。这肯定是Rational采取的方法 可以限制定义,因此只存在每个等价类的单个代表。这是how Agda defines the rationals:

record ℚ : Set where
  field
    numerator     : ℤ
    denominator-1 : ℕ
    isCoprime     : True (C.coprime? ∣ numerator ∣ (suc denominator-1))

  denominator : ℤ
  denominator = + suc denominator-1

  coprime : Coprime numerator denominator
  coprime = toWitness isCoprime

因此,它基本上只是在其依赖类型系统中编码gcd≡1条件。幸运的是那些拥有这种类型系统的人......

我不知道如何将这样的定义翻译成Haskell。应该可以以某种方式,但我怀疑这是可行的。

一个绝对可行但可能非常低效的解决方案是enumerate all rational numbers,并且只将该枚举的索引存储为单个Nat。或者更确切地说,实施所有积极理性的Calkin-Wilf树:

data PosRational = UnitRatio
                 | RatSucc PosRational
                 | RecipSucc PosRational

有趣的是如何为此定义Num等实例。我们看看......

instance Fractional PosRational where
  recip UnitRatio = UnitRatio
  recip (RecipSucc x) = RatSucc $ recip x
  recip (RatSucc x) = RecipSucc $ recip x

instance Num PosRational where
  UnitRatio + x = RatSucc x
  x + UnitRatio = RatSucc x
  RatSucc x + y = RatSucc $ x + y
  x + RatSucc y = RatSucc $ x + y
  RecipSucc (RecipSucc x) + RecipSucc (RecipSucc y)
   -- = recip (1 + 1 + x) + recip (1 + 1 + y)
   -- = (2+y + 2+x) / ((2+x)*(2+y))
   -- = (4 + x + y) / (4 + 2*x + 2*y + x*y)
   -- = 1 / (1 + (x+y+x*y)/(4+x+y))
      = RecipSucc $ (4+x+y)/(x+y+x*y)
  RecipSucc (RatSucc x) + RecipSucc (RatSucc y)
   -- = recip (1 + recip (1+x)) + recip (1 + recip (1+y))
   -- = (1+x) / (1+x + 1) + (1+y) / (1+y + 1)
   -- = ((1+x)*(2+y)+(1+y)*(2+x)) / ((2+x)*(2+y))
   -- = (2+2*x+y+x*y + 2+x+2*y+x*y) / (4+2*x+2*y+x*y)
   -- = (4 + 3*x + 3*y + 2*x*y) / (4+2*x+2*y+x*y)
   -- = (4+2*x+2*y+x*y + x+y+x*y) / (4+2*x+2*y+x*y)
   -- = 1 + (x+y+x*y) / (4+2*x+2*y+x*y)
   -- = 1 + 1 / (1 + (4+x+y)/(x+y+x*y))
      = RatSucc . RecipSucc $ (4+x+y)/(x+y+x*y)
  ...

答案 1 :(得分:2)

在数学上,整数将从自然数中定义,方法与从整数定义的有理数相同:作为一对自然数,但是定义因素。

data MyInt = I Nat Nat
-- I (Succ Zero) Zero is 1
-- I Zero (Succ Zero) is -1

现在,我们都遇到了同样的问题,即IRational都可以创建多个不同但等效的值。您可以忽略它并定义所有操作来处理它。以下是每个的简单Eq个实例:

-- for reference
instance Eq Nat where
  Zero == Zero = True
  (Succ n1) == (Succ n2) = n1 == n2
  _ == _ = False

-- No Num instance for Nat, since you can't negate a Nat
natAdd (Succ n) m = Succ (natAdd n m)
natAdd Zero m = m

instance Eq MyInt where
  (I a b) == (I c d) = a `natAdd` d == b `natAdd` c

-- Assuming an appropriate Num MyInt instance for (*)
instance Eq Rational where
  (Rational n1 d1) == (Rational n2 d2) = n1 * d2 == n2 * d1

对于其他操作,用于表示整数的实际值并不重要。以下是Num的{​​{1}}实例的一部分,我们使用这两个实例之间的相对差异,而不是关心对子本身。

MyInt

或者,您可以避免直接使用构造函数,只允许通过智能构造函数创建值。这样,只能为每个整数/有理值创建一个规范表示。

instance Num MyInt where

  negate (I a b) = I b a
  signum (I a b) | a == b = I Zero
                 | a > b = I (Succ Zero) Zero
                 | otherwise = I Zero (Succ Zero)
  abs (I a b) | a >= b = I a b
              | otherwise  = I b a
 (I a b) + (I c d) == I (a `natAdd` c) (b `natAdd` d)
 (I a b) * (I c d) == ... -- you get the idea