在使用依赖类型语言编程时,我们如何克服编译时间和运行时间差?

时间:2018-01-06 15:37:00

标签: haskell coq agda idris dependent-type

我在依赖类型系统中说过"类型"和"价值观"是混合的,我们可以将它们都视为"术语"代替。

但是我无法理解:在没有Dependent Type(如Haskell)的强类型编程语言中,在编译时决定(进行或检查)类型,但是值在运行时决定(计算或输入)。

我认为这两个阶段之间肯定存在差距。试想一下如果从STDIN以交互方式读取一个值,我们如何在必须确定AOT的类型中引用该值?

e.g。我需要从STDIN中读取一个自然数n和一个自然数xs列表(包含n个元素),如何将它们放入数据结构Vect n Nat

2 个答案:

答案 0 :(得分:43)

假设我们在运行时从STDIN输入n :: Int。然后我们读取n个字符串,并将它们存储到vn :: Vect n String中(暂时假装可以完成)。 同样,我们可以阅读m :: Intvm :: Vect m String。最后,我们连接两个向量:vn ++ vm(这里简化一点)。这可以进行类型检查,并且类型为Vect (n+m) String

现在确实,类型检查器在编译时运行,在值n,m已知之前运行,并且在vn,vm已知之前运行。但这并不重要:我们仍然可以在未知数n,m上推理符号,并认为vn ++ vm具有该类型,涉及n+m,即使我们不这样做但是知道n+m实际上是什么。

与数学不同,我们根据一些规则操纵涉及未知变量的符号表达式,即使我们不知道变量的值。我们无需知道n的{​​{1}}号码是什么号码。

同样,类型检查器可以键入check

n+n = 2*n

(好吧,实际上可能需要程序员提供一些帮助来进行类型检查,因为它涉及依赖匹配和递归。但我会忽略这一点。)

重要的是,类型检查器可以在不知道-- pseudocode readNStrings :: (n :: Int) -> IO (Vect n String) readNStrings O = return Vect.empty readNStrings (S p) = do s <- getLine vp <- readNStrings p return (Vect.cons s vp) 的情况下检查它。

请注意,多态函数实际上已经出现了同样的问题。

n

有人可能想知道“如果不知道fst :: forall a b. (a, b) -> a fst (x, y) = x test1 = fst @ Int @ Float (2, 3.5) test2 = fst @ String @ Bool ("hi!", True) ... fst将在运行时出现什么类型,那么类型检查器如何检查a”。再次,通过象征性的推理。

使用类型参数这可以说是更明显的,因为我们通常在类型擦除之后运行程序,这与上面的b之类的值参数不同,后者无法擦除。不过,普遍量化类型或n :: Int之间存在一些相似之处。

答案 1 :(得分:3)

在我看来,这里有两个问题:

  1. 鉴于某些值在编译时是未知的(例如,从STDIN读取的值),我们如何在类型中使用它们? (请注意chi has already given an excellent answer to this。)

  2. 某些操作(例如getLine)似乎在编译时完全没有意义;我们怎么可能在类型中谈论它们呢?

  3. 正如chi所说,(1)的答案是象征性的或抽象的推理。您可以读取数字n,然后通过从命令行Vect n Nat读取来构建n的过程,利用算术属性,例如{{1+(n-1) = n 1}}表示非零自然数。

    (2)的答案有点微妙。天真地,您可能想说“此函数返回长度为n的向量,其中从命令行读取n”。您可能尝试使用两种类型(如果我的Haskell符号错误,请道歉)

    unsafePerformIO (do n <- getLine; return (IO (Vect (read n :: Int) Nat)))
    

    或(以伪Coq表示法,因为我不确定Haskell对存在类型的符号是什么)

    IO (exists n, Vect n Nat)
    

    这两种类型实际上都可以理解并说出不同的东西。对我来说,第一种类型是“在编译时,从命令行读取n,并返回一个函数,该函数在运行时通过执行IO”给出长度为n的向量。第二种类型表示“在运行时,执行IO以获得自然数n和长度为n的向量”。

    我喜欢看这个的方式是所有的副作用(可能是非终止)都是monad变换器,并且只有一个monad:“真实世界”的monad。 Monad变压器在类型级别和术语级别一样工作;一件特别的事是run :: M a -> a,它在“现实世界”中执行monad(或monad变换器的堆栈)。您可以在两个时间点调用run:一个是在编译时,您可以在其中调用在类型级别显示的run的任何实例。另一个是在运行时,您可以在其中调用run的任何实例,该实例显示在值级别。请注意,run仅在指定评估顺序时才有意义;如果您的语言没有指定它是按值调用还是按名称调用(或按推送值调用或按需调用或按其他方式调用),则可能会出现不一致的情况你试着计算一种类型。