Haskell类型促销

时间:2017-02-01 17:03:06

标签: haskell

我目前正在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的自定义版本,如果我想避免使该类型递归,这会使详细程度更糟。

据我所知,没有办法在上下文中进行抽象,所以我希望有一种更简洁的方法来实现这个数字逻辑。

感谢阅读!

1 个答案:

答案 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类型不再需要重复这些情况。 (我怀疑你的TypeEnumNumber类型相互比较重复。)

data Number where
    Number :: TypeEnum a -> a -> Number

现在我们要定义一个您没有的新类型,它会将TypeEnumNum字典绑定在一起以获得相应的类型。这将是我们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函数反映(我的猜测)强制转换的方向。如果你试图为这种类型自己实现EqOrd,不考虑我的解决方案,我怀疑你会发现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定义,强制转换orderingto功能,您可以很好地使用走; liftNum可能保持不变。 (如果事实证明类型不是线性排序的,而是某种格子或类似的,那么你可以切换到Ord类。)