我现在有一些代码可以创建一个n阶素数域,其中包含计算加法,乘法和逆的所有必要函数。它工作正常,但我真的希望能够重载+, - ,*,/和^的Integral和Num中缀函数,但我不知道如何做到这一点。这是我目前的代码:
import Data.Maybe
data FiniteField = FiniteField {add::(FieldElement -> FieldElement -> FieldElement),
addinv::(FieldElement -> FieldElement),
mul::(FieldElement -> FieldElement -> FieldElement),
mulinv::(FieldElement -> FieldElement),
new::(Integer -> FieldElement)
}
newtype FieldElement = FieldElement Integer deriving (Eq, Show, Read)
toInt :: FieldElement -> Integer
toInt (FieldElement x) = x
gcdExt :: Integer -> Integer -> (Integer, Integer, Integer)
gcdExt a 0 = (1, 0, a)
gcdExt a b = (t, s - q * t, g)
where (q, r) = quotRem a b
(s, t, g) = gcdExt b r
modMulInv :: Integer -> Integer -> Integer
modMulInv a n = i
where (i, _, _) = gcdExt a n
isPrimeLoop :: Integer -> Integer -> Bool
isPrimeLoop n i
| i == n = True
| i == 2 = (mod n i /= 0) && isPrimeLoop n (i+1)
| otherwise = (mod n i /= 0) && isPrimeLoop n (i+2)
isPrime :: Integer -> Bool
isPrime n = isPrimeLoop n 2
newFiniteField :: Integer -> Maybe FiniteField
newFiniteField n
| not (isPrime n) = Nothing
| otherwise = Just (FiniteField add addinv mul mulinv new)
where
add = (\x y -> FieldElement (mod (toInt x + toInt y) n) )
addinv = (\x -> FieldElement (mod (n - toInt x) n) )
mul = (\x y -> FieldElement (mod (toInt x * toInt y) n) )
mulinv = (\x -> FieldElement (mod (modMulInv (toInt x) n) n) )
new = (\x -> FieldElement x)
答案 0 :(得分:2)
您必须解决的主要问题是您不应该被允许添加/乘法/等。不同订单下FiniteField
的值。从类型系统的角度来看,解决方案非常简单:给不同类型的不同类型的值。
newtype FieldElem (n :: Nat) = FieldElem Integer
Nat
是一种(来自GHC.TypeLits模块),其居民是类型级数字文字,如1
,2
,3
等。
现在,您有不同的类型:
FieldElem 7 -- the type of an element of a finite field of order 7
FieldElem 11 -- the type of an element of a finite field of order 11
因此,如果您尝试添加两个不同类型的值,则会出现编译错误。
> (x :: FieldElem 7) + (y :: FieldElem 11)
Error! You can only use + on two things of the same type!
> (x :: FieldElem 7) + (y :: FieldElem 7)
-- result: something of type FieldElem 7
现在您可以实现Num
实例:
instance Num (FieldElem n) where
(+) = ...
(*) = ...
这里的一个问题是(+)
需要知道订单是什么,唯一的信息是FieldElem
类型。要解决这个问题,我们要求n
成为KnownNat
类型类的实例(也来自GHC.TypeLits),这样我们就可以在运行时将其整数值作为值:
natVal :: KnownNat n => Proxy n -> Integer
所以,
> natVal (Proxy :: Proxy 10)
10
> natVal (Proxy :: Proxy 19)
19
所以我们的最终设计:(需要ScopedTypeVariables
让我们使用n
类型变量)
instance KnownNat n => Num (FieldElem n) where
FieldElem x + FieldElem y = FieldElem (mod (x + y) n)
where
n = natVal (Proxy :: Proxy n)
等!
您可以使用智能构造函数将Integer
引入FieldElem
:
mkFieldElem :: forall n. KnownNat n => Integer -> Maybe (FieldElem n)
mkFieldElem x | isPrime n = Just (FieldElem (mod x n))
| otherwise = Nothing
where
n = natVal (Proxy :: Proxy n)
好消息是你可以使用Haskell的类型推断来指定你想要的顺序:
> mkFieldElem 10 :: Maybe (FieldElem 23)
Just (FieldElem 10) -- :: Maybe (FieldElem 23)
而不是手动将其作为参数传递! :)
通过使用智能构造函数(并隐藏实际构造函数),您可以确保用户永远不会有任何类型为FieldElem 8
的值,因此您不必担心坏的字段订单加在一起。
请注意,遗憾的是,fromInteger :: KnownNat n => Integer -> FieldElem n
必然是部分的。它必须拒绝坏订单。但是 base 中有大量实例,部分实现fromInteger
无论如何:但是,fromInteger
Num
仍然是一个坏主意,Num
是一个糟糕的类型类,所以Num
的错误:)
编辑有一种潜在的方法可以使fromInteger
不是部分/全部:我们可以创建一个Prime
类型类,并且只有{{1}的实例}参数是素数:
Nat
然后你可以做:
class KnownNat n => Prime (n :: Nat)
现在,如果你有:
mkFieldElem :: Prime n => Integer -> FieldElem n
mkFieldElem x = FieldElem (mod x n)
where
n = natVal (Proxy :: Proxy n)
instance Prime n => Num (FieldElem n) where
...
fromInteger = mkFieldElem
将是一个总函数,因为唯一的实例将是素数字段!
但是,为了实现这一点,您需要以GHC可以理解的方式获取fromInteger
的实例。从理论上讲,这可以使用GHC类型检查器扩展来完成 - 您可以编写自己的类型检查器扩展,以便Prime
如果它在编译时处于初级状态,则会被赋予n
个实例。时间。但是,这还没有完成......你可以做的下一个最好的事情是提供关于质量的运行时证明:
Prime
这是使用constraints库中的witPrime :: forall n.KnownNat n => Proxy n -> Maybe (Dict (Prime n))
witPrime p | isPrime (natVal p) = Just (unsafeCoerce (Dict :: Dict (KnownNat n))
| otherwise = Nothing
,这是在运行时生成类型类实例的一种方法。如果您对类型为Dict
的值的Dict
构造函数进行了模式匹配,则实例Dict c
在"范围内"在那个案例陈述中。
在我们的案例中,我们可以这样做:
c
或者我们可以在GHCi中运行它:
case witPrime (Proxy :: Proxy 11) of
Just Dict -> ... -- in this branch, `Prime 11` is an instance we can use
Nothing -> ... -- here, it isn't
答案 1 :(得分:1)
使用此设计无法有效地使用Num
。关于类型类的重要一点是调度是按类型而不是值来完成的。 Num
的{{1}}实例无法知道它属于哪个FieldElement
,因此其操作无法取决于您正在操作的字段。
您可以采取一些方法,这可以与FiniteField
一起使用。
第一个是使Num
表达式类型使用其FieldElement
实例构建表达式树,然后可以在特定Num
内进行评估。这具有使用非常简单的技术的优点。当计算变得复杂时,它的缺点是真的对内存和性能不利。
第二种是遵循像FiniteField
这样的模式。您可以将Data.Fixed
更改为一个类,并在代表各种特定字段的一些空类型上实现它,例如名称为FiniteField
。然后使用类型参数对F17
进行参数化,该参数用于标记它们所属的FieldElement
。最后,FiniteField
Num
的实例要求其参数具有FiniteElement
实例,该实例在其实现中使用。这种方法的优点是非常适合使用。缺点是需要为每个要使用的字段设置样板FiniteField
。
第三个选项与上面的选项非常相似,但是使用某种类型级别的自然替换自定义FiniteField
类数据类型。 (手动或来自F17
)。然后,您可以根据类型级自然实现-XDataKinds
实例。这里的优点是您可以摆脱以前方法中的所有样板实例。缺点是它不要求类型级参数是素数,如果它不是素数,则几个计算是错误的。