我目前正在Write Yourself a Scheme in 48 Hours工作,并坚持进行类型宣传。
简而言之,scheme有一个数字塔(Integer-> Rational-> Real-> Complex),带有人们期望的数字促销。我用明显的
模拟了数字data Number = Integer Integer | Rational Rational | Real Double | Complex (Complex Double)
因此使用Rank2Types似乎是一种简单的方法,可以使函数在这种类型的范围内工作。对于Num a
,这看起来像
liftNum :: (forall a . Num a => a -> a -> a) -> LispVal -> LispVal -> ThrowsError LispVal
liftNum f a b = case typeEnum a `max` typeEnum b of
ComplexType -> return . Number . Complex $ toComplex a `f` toComplex b
RealType -> return . Number . Real $ toReal a `f` toReal b
RationalType -> return . Number . Rational $ toFrac a `f` toFrac b
IntType -> return . Number . Integer $ toInt a `f` toInt b
_ -> typeErr a b "Number"
虽然有效但很快变得冗长,因为每个类型类都需要一个单独的块
更糟糕的是,Complex的这种实现被简化,因为scheme可以为真实和复杂的部分使用单独的类型。实现这一点需要一个包含两个Number
的自定义版本,如果我想避免使该类型递归,这会使详细程度更糟。
据我所知,没有办法在上下文中进行抽象,所以我希望有一种更简洁的方法来实现这个数字逻辑。
感谢阅读!
答案 0 :(得分:12)
这是一个提案。我们希望您的typeEnum
函数执行的主要操作是将Num a
字典放入范围。因此,让我们使用GADT来实现这一目标。我会简化一些事情,以便更容易解释这个想法并编写代码,但没有必要:我会专注于Number
而不是LispVal
而我赢了&#39 ; t出现问题时报告详细错误。首先是一些样板:
{-# LANGUAGE GADTs #-}
{-# LANGUAGE Rank2Types #-}
import Control.Applicative
import Data.Complex
现在,您没有给出类型枚举的定义。但是我会给出我的,因为它是秘密的一部分:我的类型枚举将通过GADT在Haskell的术语级别和Haskell的类型级别之间建立联系。
data TypeEnum a where
Integer :: TypeEnum Integer
Rational :: TypeEnum Rational
Real :: TypeEnum Double
Complex :: TypeEnum (Complex Double)
由于这种联系,我的Number
类型不再需要重复这些情况。 (我怀疑你的TypeEnum
和Number
类型相互比较重复。)
data Number where
Number :: TypeEnum a -> a -> Number
现在我们要定义一个您没有的新类型,它会将TypeEnum
与Num
字典绑定在一起以获得相应的类型。这将是我们typeEnum
函数的返回类型。
data TypeDict where
TypeDict :: Num a => TypeEnum a -> TypeDict
ordering :: TypeEnum a -> Int
ordering Integer = 0 -- lowest
ordering Rational = 1
ordering Real = 2
ordering Complex = 3 -- highest
instance Eq TypeDict where TypeDict l == TypeDict r = ordering l == ordering r
instance Ord TypeDict where compare (TypeDict l) (TypeDict r) = compare (ordering l) (ordering r)
ordering
函数反映(我的猜测)强制转换的方向。如果你试图为这种类型自己实现Eq
和Ord
,不考虑我的解决方案,我怀疑你会发现GHC为GADT派生这些类的原因。 (至少,我花了几次尝试!显而易见的定义没有进行类型检查,而且稍微不那么明显的定义有错误的行为。)
现在我们准备编写一个为数字生成字典的函数。
typeEnum :: Number -> TypeDict
typeEnum (Number Integer _) = TypeDict Integer
typeEnum (Number Rational _) = TypeDict Rational
typeEnum (Number Real _) = TypeDict Real
typeEnum (Number Complex _) = TypeDict Complex
我们还需要铸造功能;你基本上可以在这里连接toComplex
和朋友的定义:
-- combines toComplex, toFrac, toReal, toInt
to :: TypeEnum a -> Number -> Maybe a
to Rational (Number Integer n) = Just (fromInteger n)
to Rational (Number Rational n) = Just n
to Rational _ = Nothing
-- etc.
to _ _ = Nothing
一旦我们掌握了这种机制,liftNum
就会出乎意料地缩短。我们只需找到要转换的适当类型,获取该类型的字典,然后执行强制转换和操作。
liftNum :: (forall a. Num a => a -> a -> a) -> Number -> Number -> Maybe Number
liftNum f a b = case typeEnum a `max` typeEnum b of
TypeDict ty -> Number ty <$> liftA2 f (to ty a) (to ty b)
此时你可能会抱怨:你的最终目标是liftNum
中每个班级实例没有一个案例,而且我们已经实现了这个目标,但看起来我们只是把它推到了typeEnum
的定义,每个类实例有一个案例。但是,我为自己辩护:你没有向我们展示你的typeEnum
,我怀疑每个班级实例已经有一个案例。因此,我们在liftNum
以外的函数中没有产生任何新成本,并且确实大大简化了liftNum
。这也为更复杂的Complex
操作提供了平滑的升级路径:扩展TypeEnum
定义,强制转换ordering
和to
功能,您可以很好地使用走; liftNum
可能保持不变。 (如果事实证明类型不是线性排序的,而是某种格子或类似的,那么你可以切换到Ord
类。)