防止Haskell中的类型变量扩散

时间:2017-10-13 22:16:29

标签: haskell types ghc existential-type

参数化类型变量很不错,但它没有扩展。作为可能发生的事情的一个例子,http://oleg.fi/gists/posts/2017-04-26-indexed-poptics.html给出了一个包含9个类型变量的抽象。我一直致力于程序转换的框架,这些框架由编程语言参数化,并且可以想象将来会有数十个或数百个参数。

所以这里是基本问题:我有一个数据类型T,它通过N种类型进行参数化。如何在T上编写函数,而不是每次使用时都写下N型变量?

以下是我看过的一些方法,其中没有一种方法可以满足:

对类型* -> *

的类型变量进行参数化
data V = Var1 | Var2 | Var3 | Var4

myfunc :: forall (v :: V -> *). Constraints v => v Var1 -> v Var2
myfunc = ...

所以,现在,我不需要参数化超过4种类型变量Var1, Var2, Var3, Var4,而只需要对V -> *种类型变量进行参数化。

这是有效的,但在此示例中,myfunc无法调用,因为v无法推断。您需要将其更改为Proxy v -> v Var1 -> v Var2。然后,每次要将myfunc与差异变量一起使用时,您需要定义一个单独的GADT,并使用自己的样板。像这样:

data MyV a where
  MyVar1 :: Int -> MyV Var1
  MyVar2 :: String -> MyV Var2
  MyVar3 :: Bool -> MyV Var3
  MyVar4 :: [Int] -> MyV Var4

毋庸置疑,这是非常不满意的。

这种方法正是compdata library的多重排序部分所采用的方法。它在那里非常好,因为这个样板与你正在编写的正常数据类型完全一致。

(*,*,*,...)种类型变量或记录类型进行参数化

{-# LANGUAGE TypeInType #-}
import Data.Kind

type Vars = (*,*,*,*)

myfunc :: forall (v :: Vars). ...
myfunc = ...

这不起作用,因为据我所知,没有办法破坏类型v的类型变量(*,*,*,*)。我可以创建一个实例(Int, String, Bool, [Int]),但我实际上无法提取组件Int,String等。我能做的最好就是写一个类型族:

type family Fst (v :: Vars) where
  Fst '(a,b,c,d) = a
type family Snd ....

myfunc :: forall (v :: Vars). Fst Vars -> Snd Vars

但是,这与前一个解决方案存在同样的问题:除非您单独传入v,否则无法推断Proxy v。我也尝试添加约束type Extensional v = (v ~ '(Fst v, Snd v, Third v, Fourth v)),但它没有帮助。

使用存在量化变量

data HasFourTypeVariables a b c d = ....
data IOnlyCareAboutTwo a b = forall c d. IOnlyCareAboutTwo (HasFourTypeVariables a b c d)

这不起作用。以下是您尝试使用它时的样子:

update :: IOnlyCareAboutTwo a b -> IOnlyCareAboutTwo a b
update = ...

useUpdate :: HasFourTypeVariables a b c d -> HasFourTypeVariables a b c d
useUpdate x = case update (IOnlyCareAboutTwo x) of
                IOnlyCareAboutTwo y -> y

这不是一个类型检查,因为类型检查器不知道update的输入和输出具有相同的存在性证据。

使用背包

到目前为止,背包看起来是最好的竞争者。取决于具有5种类型和相关操作/约束的模块签名,有点像在每个操作中具有5个通用量化的约束类型变量,除非您不需要将它们写下来。

背包仍然相当新,尚未与Stack集成;我还没有任何经验。此外,它似乎是为整个包而不是较小的功能单元进行参数化而构建的;我认为它对这里需要的显式实例化的支持很差。

有一长串的类型变量,并忍受它

解决方案我考虑过这样做。 :(

一些背景

这个问题在我的多语言程序转换工作中有可能出现,扩展https://arxiv.org/pdf/1707.04600.pdf

在我的系统中,编程语言中的术语具有Term f l类型,其中f是编程语言的签名(种类(*->*)->(*->*) - 请参阅{{3}为了解释),l是一种排序。因此,C语句中的语句可能具有类型Term CSig StmtL,而Java表达式可能具有类型Term Java ExpL

到目前为止,这只是两个类型变量。但是,随着我将系统推向更具普遍性以及抽象越来越深的语义属性的能力,类型变量的数量可能会爆炸。以下是一些示例:

  1. 我想在AST节点上存储注释。有些像节点标签及其来源,在每棵树上都是不错的想法,但是其他一些,如符号解析,或者是否已修改该子树的标记,在某些树中是可取的,而在其他树中则不是。因此,我喜欢我的陈述能够在某些表示中灵活地添加注释而不能在其他表示中添加注释,并且能够编写仅关注这些注释的子集是否存在的运算符。这该怎么做?注释的一个或多个类型变量,以及很多" HasSymbolResolutionAnnotation a"约束

  2. 经过大量的经验和许多小时的反思,我已经确定可变的AST实际上是个不错的主意。然后,我希望能够编写可以处理纯粹和可变AST的运算符。我还没弄清楚如何最好地做到这一点,但你可以打赌它会在我的类型中添加至少一个类型变量。

  3. CSigJavaSig签名中,可能有许多语言通用节点,例如"添加。"对于许多简单的分析和转换,仅仅说#34;这种语言已经添加了"并坚持使用通用添加节点。但是对于更复杂的问题,你的语言中的加法是否会溢出以及如何(+)运算符是基元还是可以像C ++或Haskell中那样被覆盖,以及它对周围类型的约束是否有问题。现在,而不是"添加"节点,您的语言可能有一个Add MonomorphicAddition NonOverridable OverflowWrapsToNegative节点,您的符号分析定义了具有Add a b OverflowWrapsToNegative节点的语言的传递函数。您可以像这样编码的运算符的变化数量没有限制。只要有很多有用的东西你可以说这个参数化的运算符只引用它的几个参数,那么最好以这种方式对它们进行处理。

  4. 我希望这有助于解释为什么这是一个问题。

2 个答案:

答案 0 :(得分:3)

这个答案的灵感来自Trees That Grow论文。它假设只存在有限数量的"变体"对于数据类型,只使用 N 参数的所有可能实例化的一小部分。

{-# language DataKinds #-}
{-# language TypeFamilies #-}
{-# language KindSignatures #-}
{-# language PolyKinds #-}
{-# language FlexibleContexts #-}

我们定义这样的数据类型

data MyType (v::Variant) = MyType (Xa v) (Xb v) (Xc v) Int

data Variant = V1 | V2

type family Xa (v::Variant) :: * where
    Xa V1 = Int
    Xa V2 = Bool

type family Xb (v::Variant) :: * where
    Xb V1 = Bool
    Xb V2 = Char

type family Xc (v::Variant) :: * where
    Xc V1 = String
    Xc V2 = Maybe Int

有两个"变种"数据类型。每个不断变化的领域都有自己的(公认的锅炉)类型系列,它将变量映射到该变体中的实际类型。

这是一个简单的函数,适用于数据类型的所有变体:

getInt :: MyType (v :: Variant) -> Int
getInt (MyType _ _ _ i) = i

使用-XConstraintKinds,我们可以定义所有字段共享的约束:

{-# language ConstraintKinds #-}
import GHC.Exts (Constraint)
type MyForAll (p :: * -> Constraint) (v::Variant) = (p (Xa v),p (Xb v),p (Xc v))

我们可以用它来定义像

这样的函数
myShow :: MyForAll Show (v :: Variant) => MyType v -> String
myShow (MyType a b c i) = show a ++ show b ++ show c ++ show i

我们还可以启用-XTypeApplications来指定变体:

λ :t MyType
MyType :: forall {v :: Variant}. Xa v -> Xb v -> Xc v -> Int -> MyType v
λ :set -XTypeApplications
λ :t MyType @V1
MyType @V1 :: Int -> Bool -> String -> Int -> MyType 'V1

答案 1 :(得分:2)

如果要将多个类型变量组合在一起,则可以执行此操作。 在值级别上,您只需使用记录,也可以在类型级别上执行此操作。创建一个记录类型,然后可以使用它的提升版本来分组类型。记录访问变得有点笨拙,因为值级别记录选择器语法不会提升。

这是一个应该澄清我的意思的例子。

{-# LANGUAGE StandaloneDeriving, TypeInType, UndecidableInstances #-}
module RecordTyVars where
import Data.Kind

-- The normal way, with 3 type variables.
data OExpr sym lit op = OVar sym | OLit lit | OPrimOp op [OExpr sym lit op]
     deriving (Show)

oe :: OExpr String Integer Op
oe = OPrimOp Add [OVar "x", OLit 1]

data Op = Add | Sub
     deriving (Show)

--------

-- Record that when lifted will contain the types.
data ExprTypes = Types Type Type Type

-- Record access functions, since the record syntax doesn't lift.
type family SymType (r :: ExprTypes) :: * where
    SymType ('Types sym lit op) = sym
type family LitType (r :: ExprTypes) :: * where
    LitType ('Types sym lit op) = lit
type family OpType (r :: ExprTypes) :: * where
    OpType  ('Types sym lit op) = op

-- Using the record of types
data Expr r = Var (SymType r) | Lit (LitType r) | PrimOp (OpType r) [Expr r]

-- Must use standalone deriving when thing the going gets tough.
deriving instance (Show (SymType r), Show (LitType r), Show (OpType r)) =>
                  Show (Expr r)

e :: Expr ('Types String Integer Op)
e = PrimOp Add [Var "x", Lit 1]