我有一个数据结构,例如表达式树或图形。我想添加一些"测量"功能,例如depth
和size
。
如何最好地输入这些功能?
我看到以下三种变体具有大致相同的用途:
depth :: Expr -> Int
depth :: Expr -> Integer
depth :: Num a => Expr -> a
我有以下注意事项:
我以base
和fgl
为例,他们一直使用Int
,但Data.List
也有genericLength
等功能返回类型中的多态性,我认为这些generic
函数的添加可能是现代化趋势的反映,我可能应该尊重和强化。
当一些广泛使用的库提供一组具有相同功能的全面功能时,类似的思维运动是显而易见的,当用户需要几种可能的返回类型选择时(例如{{1}提供接受xml-conduit
或ByteString
的惰性和严格类型的解析器。
Text
是一个比Integer
更好的类型,我有时会发现我需要将一个列表的长度转换为Int
,这是因为运算的algorighm在Integer
中需要考虑这个长度。
使函数返回Integer
意味着这些函数变为多态,并且可能会降低性能。我不太了解所有细节,但据我所知,可能会有一些运行时成本,而且多态事物更难记忆。
什么是最受欢迎的做法?哪个部分是由于遗留和兼容性考虑因素? (即如果今天设计Integral
,Data.List
等功能会有哪些类型?)我是否错过了任何利弊?
答案 0 :(得分:6)
简短回答:作为一般规则使用Int
,如果您需要将其转换为其他内容,请使用fromIntegral
。 (如果您发现自己进行了很多转换,请定义fi = fromIntegral
以保存输入,或者创建自己的包装器。)
主要考虑因素是表现。您想编写算法,以便在内部使用有效的整数类型。如果Int
足够大,无论你做什么计算(标准保证有符号的30位整数,但即使在使用GHC的32位平台上,它是一个带符号的32位整数),你可以假设它将是平台上的高速整数类型,特别是与Integer
(具有无法优化的拳击和bignum计算开销)相比较。请注意,性能差异可能很大。与Int
相比,使用Integer
s的简单计数算法通常会快5-10倍。
虽然可以为您的功能提供不同的签名:
depth :: Expr -> Integer
depth :: (Num a) => Expr -> a
但实际上是使用高效的Int
类型在引擎盖下实现它并在最后进行转换,这使得转换隐含在我看来是不好的做法。特别是如果这是一个库函数,通过使其成为签名的一部分,明确表示内部使用Int
会让我更加明智。
关于您列出的注意事项:
首先,generic*
中的Data.List
函数不是现代的。特别是,genericLength
在1996年7月发布的GHC 0.29中提供了length
。在之前的某个时间点,genericLength
已根据 length :: [a] -> Int
length = genericLength
定义 ,简单地说:
#ifdef USE_REPORT_PRELUDE
但是在GHC 0.29中,这个定义用length
注释掉了,generic*
的几个手动优化变体是独立定义的。其他Prelude
函数不是0.29,但GHC 4.02(1998)已经存在。
最重要的是,当length
Foldable
版本的genericLength
从列表推广到ByteString
时,哪个 是一个相当近期的开发(自GHC 7.10起? ),没有人关心Text
做任何事情。我也不认为我曾经在任何严肃的Haskell代码中看到过“在野外”使用过的这些函数。在大多数情况下,您可以将它们视为已弃用。
其次,在库中使用lazy / strict和conduit-xml
/ ByteString
变体代表了一种不同的情况。特别是,Text
用户通常会根据有关正在处理的数据的考虑因素以及算法的构造,在惰性和严格变体之间以及conduit-xml
和Text
类型之间做出决定。对于给定程序的整个类型系统而言,它具有深远的影响力。如果将ByteString
与惰性Text
类型一起使用的唯一方法是将其零碎地转换为严格Int
s,则将其传递给库,然后将其拉出并将其转换回来对于懒惰的depth
类型,没有人会接受这种复杂性。相比之下,fromInteger . depth
的单态Integer
定义可以正常工作,因为您需要depth
来使其适应任何数字上下文。
第三,如上所述,count
只是在你不关心性能的情况下具有任意精度的观点,它只是一种“更好”的类型。对于Data.List
和Int
这样的实际情况,性能可能比无限精度更重要。
第四,我不认为在这里选择多态或非多态版本时,运行时成本或失败记忆应该是严肃的考虑因素。在大多数情况下,GHC将在memoization没有问题的上下文中生成多态函数的专用版本。
在此基础上,我怀疑如果今天设计{{1}},它仍会使用{{1}}。
答案 1 :(得分:3)
我同意K. A. Buhr的所有观点,但这里有更多的答案:
如果您希望支持某种不适合内存的表达式树(这似乎很有趣,但不太可能),您应该使用返回类型Integer
。如果我看到Expr -> Integer
,我会查看代码或文档,试图了解codomain如何或为何如此大。
重新。性能Integer
:如果数字不大于机器字的最大宽度,将使用正常的机器字算法。简化,类型基本上是:
data Integer = SmallInteger Int | LargeInteger ByteArray
ķ。 A. Buhr提到存在一个不可避免的性能损失,即该值不能被取消装箱(即它将始终具有堆表示,并且将被读取并写入存储器),这听起来对我来说是正确的。< / p>
相比之下,Int
(或Word
)上的功能通常是未装箱的,因此在核心中您会看到类似Int# -> Int# ->
的类型。您可以将Int#
视为仅存在于机器寄存器中。如果您关心性能,这就是您希望数字代码的样子。
重新。多态版本:根据方便的类型推断,围绕具体的数字输入和多态数字输出设计库可能没问题。我们已经在某种程度上已经有了这个,因为数字文字被重载了。有些时候文字(例如,当-XOverloadedStrings
时也需要字符串文字)需要给出类型签名,所以我希望如果base
设计为更多态,你会遇到更多需要这种情况的场合(fromIntegral
的用途较少)。
您未提及的另一个选项是使用Word
来表示depth
是非负面的。这对输入更有用,但即使这样也常不值得:Word
仍会溢出,负面文字有效(尽管GHC会发出警告);在某种程度上,它只是在错误发生的地方移动。