我一直在为一个简单的表达式语言尝试一个赋值函数,主要是为了熟悉一些我以前没有用过的Haskell扩展。最新的实验是使用DataKinds和TypeFamilies来统一一元和二元运算符。经过一些麻烦之后,我使它工作了,但前提是我用一个定制的自然数定义来表示运算符的arity:
data NatT = ZeroT | SuccT NatT
如果我尝试使用TypeNats,编译器将无法匹配类型。下面是失败的版本。这是apply
函数的第二行,编译器对此感到不满-f
到a
的应用程序。错误是无法将Double -> FuncN (n - 1)
与FuncN n
匹配。(这正好是FuncN定义的第二行)。我使用n+1
和n
代替n
和n-1
会得到不同的错误。我还尝试将FuncN声明为单射的,但编译器也不喜欢这样。
这个问题并没有让我感到惊讶,我对使用NatT
的工作版本感到满意,但我有兴趣看看是否有可能在使用TypeNats的同时解决这个问题。
{-#LANGUAGE FlexibleInstances, GADTs, DataKinds, TypeFamilies, TypeOperators, UndecidableInstances #-}
module FourFours
( NularyOp(..)
, UnaryOp(..)
, BinaryOp(..)
, Term(..)
, ListN(..)
, eval
) where
import GHC.TypeNats
type family FuncN n where
FuncN 0 = Double
FuncN n = Double -> FuncN (n - 1)
data ListN n a where
EmptyN :: ListN 0 a
ConsN :: a -> ListN (n - 1) a -> ListN n a
instance Functor (ListN n) where
fmap f EmptyN = EmptyN
fmap f (ConsN a as) = ConsN (f a) (fmap f as)
apply :: FuncN n -> ListN n Double -> Double
apply x EmptyN = x
apply f (ConsN x xs) = apply (f x) xs
data NularyOp = Four | FortyFour | PointFour deriving (Eq, Ord, Enum)
data UnaryOp = Sqrt deriving (Eq, Ord, Enum)
data BinaryOp = Add | Sub | Mul | Div | Pow deriving (Eq, Ord, Enum)
class Op o where
type Arity o :: Nat
evalOp :: o -> FuncN (Arity o)
instance Op NularyOp where
type Arity NularyOp = 0
evalOp Four = 4
evalOp FortyFour = 44
evalOp PointFour = 0.4
instance Op UnaryOp where
type Arity UnaryOp = 1
evalOp Sqrt = sqrt
instance Op BinaryOp where
type Arity BinaryOp = 2
evalOp Add = (+)
evalOp Sub = (-)
evalOp Mul = (*)
evalOp Div = (/)
evalOp Pow = (**)
data Term n where
OpTerm :: Op o => o -> Term (Arity o)
Apply :: Term n -> ListN n (Term 0) -> Term 0
eval :: Term n -> FuncN n
eval (OpTerm o) = evalOp o
eval (Apply o ts) = apply (eval o) (fmap eval ts)
2条答案
按热度按时间tktrz96b1#
这里有一个解决这个问题的方法。这个方法有点局限--在这种情况下适用,因为所讨论的特定程序只使用了Nat中的几个类型。它也是低效的。但它至少避免了使用像
unsafeCoerce
或OverlappingInstances
这样的讨厌的东西。GHC的困难在于处理
FuncN
类型族。要对apply
函数的ConsN
子句的右侧进行类型检查,GHC需要推导出FuncN (n + 1) ~ Double -> FuncN n
。当特定类型(0
,1
,2
...)替代了n
,但无法推导出具有类型变量n
的一般形式。您可以考虑使用
FuncN (n + 1) = Double -> FuncN n
作为FuncN
定义的第二个子句,但这是不允许的,因为操作符(+)
不是构造函数:这是一个类型族,即使可以这样做,我也不认为会有什么帮助,因为GHC甚至不能排除FuncN
的第一个子句适用于n + 1
的可能性.解决这个问题的技巧是生成类型相等的证据,以命题相等项
FuncN (n + 1) :~: Double -> FuncN n
的形式。我们为函数定义了一个类型别名,该函数将Nat
中特定类型的代理Map到相应的证据上。我们需要
Typeable
约束,这样才能使用eqT
。我还没有找到构造这种类型的全函数的方法,但这里有一个只包含
0
和1
的方法,这是我们在这个特定程序中唯一需要的。eqT
测试允许GHC分别考虑0
和1
情况。返回的Refl
值是GHC可以进行类型检查的证据,因为它可以推导出Nat
中特定的单个类型的FuncN (n + 1) ~ Double -> FuncN n
。然后,我们更新apply的第二个子句,以使用此证据
...在
ConsN
模式子句中,我们还将n'
与该类型匹配。使用该类型,我们构造一个代理传递给funcNProp
,该代理生成对apply (f x) xs
进行类型检查所需的证据。以下是完整的计划:
31moq8wy2#
事实证明,还有另一种解决方案,只需添加一个仔细放置的约束。
如前所述,GHC的难点在于处理
FuncN
类型族。要对apply
函数的ConsN
子句的右侧进行类型检查,GHC需要推导出FuncN (n + 1) ~ Double -> FuncN n
。当特定类型(0
,1
,2
...)替代了n
,但无法用类型变量n
推导出一般形式。解决方案是将该约束添加到
ListN
数据定义的ConsN
子句中。也就是说,实际上,只承诺构造满足该属性的列表。由于该属性是一个同义反复,因此这不是一个限制,但是,更重要的是,如果所有ListN
构造都是文字的(因此TypeNat是特定示例),GHC能够建立该属性,这是该特定程序的情况。整个程序: