这是一个场景:我写了一些带有类型签名的代码,GHC抱怨不能对某些x和y推断x ~ y。你通常可以扔GHC一根骨头,简单地将同构添加到函数约束中,但这是一个坏主意,原因如下:

它不强调理解代码。 您最终可以得到5个约束,其中一个约束就足够了(例如,如果这5个约束是由一个更具体的约束暗示的) 如果你做错了什么,或者GHC没有帮助,你就会得到虚假的约束

我刚花了几个小时处理三号病例。我正在使用syntax -2.0,并试图定义一个与域无关的共享版本,类似于NanoFeldspar.hs中定义的版本。

我有这个:

{-# LANGUAGE GADTs, FlexibleContexts, TypeOperators #-}
import Data.Syntactic

-- Based on NanoFeldspar.hs
data Let a where
    Let :: Let (a :-> (a -> b) :-> Full b)

share :: (Let :<: sup,
          Domain a ~ sup,
          Domain b ~ sup,
          SyntacticN (a -> (a -> b) -> b) fi) 
      => a -> (a -> b) -> a
share = sugarSym Let

GHC不能推导(内部a) ~(内部b),这当然不是我想要的。所以,要么是我写了一些我不打算写的代码(这需要约束),要么是GHC因为我写的其他一些约束而想要那个约束。

原来我需要添加(句法a,句法b,句法(a->b))到约束列表,其中没有一个暗示(内部a) ~(内部b)。我基本上偶然发现了正确的约束;我仍然没有一个系统的方法来找到它们。

我的问题是:

Why did GHC propose that constraint? Nowhere in syntactic is there a constraint Internal a ~ Internal b, so where did GHC pull that from? In general, what techniques can be used to trace the origin of a constraint which GHC believes it needs? Even for constraints that I can discover myself, my approach is essentially brute forcing the offending path by physically writing down recursive constraints. This approach is basically going down an infinite rabbit hole of constraints and is about the least efficient method I can imagine.

Haskell(带有GHC编译器)比您预期的要快得多。如果使用得当,它可以接近低级语言。(Haskellers最喜欢做的一件事是尝试在C语言的5%之内(甚至超过它,但这意味着你在使用一个低效的C程序,因为GHC将Haskell编译为C语言)。)我的问题是,为什么?

Haskell是声明性的,并且基于lambda演算。机器架构显然是必要的,大致上基于图灵机。事实上,Haskell甚至没有一个具体的评估顺序。此外,与处理机器数据类型不同,您一直都在使用代数数据类型。

最奇怪的是高阶函数。您可能会认为,动态地创建函数并将它们到处扔,会使程序变慢。但是使用高阶函数实际上使Haskell更快。实际上,为了优化Haskell代码,你需要让它更优雅和抽象,而不是更像机器。Haskell的任何高级特性如果不加以改进,似乎都不会影响它的性能。

抱歉,如果这听起来是废话,但这是我的问题:为什么Haskell(用GHC编译)这么快,考虑到它的抽象性质和与物理机器的差异?

注意:我之所以说C和其他命令式语言有点类似于图灵机(但没有到Haskell类似于Lambda Calculus的程度),是因为在命令式语言中,您有有限数量的状态(也就是行号),以及一个磁带(ram),这样状态和当前磁带就决定了对磁带做什么。关于从图灵机到计算机的转变,请参阅维基百科的条目,图灵机等价物。

我开始理解forall关键字是如何在所谓的“存在类型”中使用的,比如:

data ShowBox = forall s. Show s => SB s

然而,这只是forall用法的一个子集,我根本无法理解它在这样的情况下的用法:

runST :: forall a. (forall s. ST s a) -> a

或者解释为什么它们不同:

foo :: (forall a. a -> a) -> (Char, Bool)
bar :: forall a. ((a -> a) -> (Char, Bool))

或者整个RankNTypes的东西…

我倾向于使用清晰、无专业术语的英语,而不是学术环境中常见的语言。我试图阅读的大部分解释(我可以通过搜索引擎找到的)都有这些问题:

They're incomplete. They explain one part of the use of this keyword (like "existential types") which makes me feel happy until I read code that uses it in a completely different way (like runST, foo and bar above). They're densely packed with assumptions that I've read the latest in whatever branch of discrete math, category theory or abstract algebra is popular this week. (If I never read the words "consult the paper whatever for details of implementation" again, it will be too soon.) They're written in ways that frequently turn even simple concepts into tortuously twisted and fractured grammar and semantics.

所以…

回到真正的问题上来。有谁能用清楚明了的英语完全解释forall关键字(或者,如果它存在的话,指出一个我错过的如此清晰的解释),而不假设我是一个沉浸在行话中的数学家吗?

来自GHC 7.6的文档:

你通常一开始就不需要使用specialized语法。当编译模块M时,GHC的优化器(带-O)会自动考虑在M中声明的每个顶级重载函数,并针对在M中被调用的不同类型对其进行特殊化。优化器还会考虑每个导入的INLINABLE重载函数,并针对在M中被调用的不同类型对其进行特殊化。

and

此外,给定函数f的specialspecialpragma, GHC将自动为f调用的任何类型类重载函数创建专门化,如果它们与specialspecialpragma在同一个模块中,或者如果它们是INLINABLE;以此类推。

因此,GHC应该自动专门化一些/大多数/所有(?)标记为INLINABLE的函数而不使用pragma,如果我使用显式的pragma,专门化是可传递的。我的问题是: 自动专门化是可传递的吗?

具体来说,这里有一个小例子:

Main.hs:

import Data.Vector.Unboxed as U
import Foo

main =
    let y = Bar $ Qux $ U.replicate 11221184 0 :: Foo (Qux Int)
        (Bar (Qux ans)) = iterate (plus y) y !! 100
    in putStr $ show $ foldl1' (*) ans

foo。:

module Foo (Qux(..), Foo(..), plus) where

import Data.Vector.Unboxed as U

newtype Qux r = Qux (Vector r)
-- GHC inlines `plus` if I remove the bangs or the Baz constructor
data Foo t = Bar !t
           | Baz !t

instance (Num r, Unbox r) => Num (Qux r) where
    {-# INLINABLE (+) #-}
    (Qux x) + (Qux y) = Qux $ U.zipWith (+) x y

{-# INLINABLE plus #-}
plus :: (Num t) => (Foo t) -> (Foo t) -> (Foo t)
plus (Bar v1) (Bar v2) = Bar $ v1 + v2

GHC专门化对加号的调用,但对Qux Num实例不专门化(+),这会降低性能。

然而,一个显式的pragma

{-# SPECIALIZE plus :: Foo (Qux Int) -> Foo (Qux Int) -> Foo (Qux Int) #-}

如文档所示,结果是传递专门化,所以(+)是专门化的,代码速度快了30倍(都是用-O2编译的)。这是预期的行为吗?我应该只期望(+)通过显式的pragma传递地特殊化吗?


更新

7.8.2的文档没有改变,行为是一样的,所以这个问题仍然是相关的。