Haskell就是关于抽象的。但是,由于堆上所有抽象(多态)数据指针的公共表示,抽象会花费我们额外的CPU周期和额外的内存使用。有一些方法可以使抽象代码在高性能要求下更好地发挥作用。据我所知,完成这项工作的一种方法是专门化——基本上是额外的代码生成(手动或通过编译器),对吗?
让我们假设下面的所有代码都是Strict(这有助于编译器执行更多优化?)
如果我们有一个函数sum
:
sum :: (Num a) => a -> a -> a
我们可以使用specialize
pragma:生成它的专用版本
{-#SPECIALIZE sum :: Float -> Float -> Float#-}
现在,如果haskell编译器可以在编译时确定我们在两个Float
上调用sum
,那么它将使用它的专用版本。没有堆分配,对吧?
功能-已完成。相同的杂注可以应用于类实例。这里的逻辑不会改变,是吗?
但是数据类型呢?我怀疑TypeFamilies
在这里负责?
让我们尝试专门化依赖长度的索引列表。
--UVec for unboxed vector
class UVec a where
data Vec (n :: Nat) a :: *
instance UVec Float where
data Vec n Float where
VNilFloat :: Vec 0 Float
VConsFloat :: {-#UNPACK#-}Float ->
Vec n Float ->
Vec (N :+ 1) Float
但是Vec
有一个问题。我们不能将其构造函数的模式匹配为CCD_ 7的每个实例不必为CCD_。这迫使我们为Vec
的每个实例在Vec
上实现每个函数(因为缺乏模式匹配意味着它在Vec
上不可能是多态的)。在这种情况下,最佳做法是什么?
正如您所说,如果不知道a
是什么,我们就无法在UVec a
上进行模式匹配。一种选择是使用另一个类型类,用自定义函数扩展向量类。
class UVec a => UVecSum a where
sum :: UVec a -> a
instance UVecSum Float where
sum = ... -- use pattern match here
如果稍后在v :: UVec Float
中使用sum v
,则将调用我们在实例中定义的特定于Float
的代码。
部分答案,但可能会有所帮助。
据我所知,完成这项工作的一种方法是专业化——基本上是额外的代码生成(手动或编译器),对吗?
是的,这类似于C++模板中的代码实例化。
如果haskell编译器能够在编译时确定我们对两个浮点调用sum,那么它将使用它的专用版本。没有堆分配,对吧?
是的,编译器会尽可能调用专用版本。不确定您对堆分配的意思。
关于依赖类型的向量:通常(我从Idris中知道这一点),编译器会在可能的情况下消除向量的长度。它用于更强的类型检查。在运行时,长度信息是无用的,并且可以被丢弃。