Haskell类型阻碍了简单的“平均”功能


76

我正在与初学者Haskell玩耍,我想编写一个平均函数。好像是世界上最简单的事情,对吧?

错误。

似乎Haskell的类型系统禁止平均值在通用数字类型上工作-我可以让它在整数列表或小数列表上工作,但不能同时在两者之间工作。

我想要:

average :: (Num a, Fractional b) => [a] -> b
average xs = ...

但是我只能得到:

averageInt :: (Integral a, Fractional b) => [a] -> b
averageInt xs = fromIntegral (sum xs) / fromIntegral (length xs)

要么

averageFrac :: (Fractional a) => [a] -> a
averageFrac xs = sum xs / fromIntegral (length xs)

第二个似乎可行。直到我尝试传递变量。

*Main> averageFrac [1,2,3]
2.0
*Main> let x = [1,2,3]
*Main> :t x
x :: [Integer]
*Main> averageFrac x

<interactive>:1:0:
    No instance for (Fractional Integer)
      arising from a use of `averageFrac ' at <interactive>:1:0-8
    Possible fix: add an instance declaration for (Fractional Integer)
    In the expression: average x
    In the definition of `it': it = averageFrac x

显然,Haskell对它的类型确实很挑剔。那讲得通。但是当他们俩都可以成为[Num]时

我是否错过了RealFrac的明显应用程序?

有没有办法强制积分到分数输入时不会阻塞的分数?

有没有什么方法可以使用Eithereither制作某种可以在任何数值数组上使用的多态平均函数?

Haskell的类型系统是否会彻底禁止该功能?

学习Haskell就像学习微积分。它真的很复杂,并且是基于大量理论,有时问题是如此令人难以置信,以至于我什至不知道如何正确地表达问题,因此任何见解都会被热烈接受。

(此外,脚注:这是基于作业问题。每个人都同意上面的averageFrac可以得到满分,但是我暗中怀疑是否有一种方法可以使它同时在整数和小数数组上工作)


Answers:


105

因此,从根本上讲,您会受到(/)类型的限制:

(/) :: (Fractional a) => a -> a -> a

顺便说一句,您还需要Data.List.genericLength

genericLength :: (Num i) => [b] -> i

那么,如何从更一般的内容中删除fromIntegral:

import Data.List

average xs = realToFrac (sum xs) / genericLength xs

它只有一个Real约束(Int,Integer,Float,Double)...

average :: (Real a, Fractional b) => [a] -> b

这样就可以将任何Real转换为小数。

并注意所有海报都被Haskell中的多态数字文字所抓住。1不是整数,而是任何数字。

Real类仅提供一种方法:将Num类中的值转换为有理数的能力。这正是我们这里需要的。

因此,

Prelude> average ([1 .. 10] :: [Double])
5.5
Prelude> average ([1 .. 10] :: [Int])
5.5
Prelude> average ([1 .. 10] :: [Float])
5.5
Prelude> average ([1 .. 10] :: [Data.Word.Word8])
5.5

但是,如果我想在“双打”列表中称“平均值”怎么办?是否有一个类似于numToFrac的函数,该函数采用Real或Fractional并返回Fractional?我们可以写一个吗?
jakebman

5
您可以向其输入Doubles列表,因为Double是真实的。“平均值([1 .. 10] :: [Double])”。Real类恰好增加了从Num中的事物构造合理价值的能力。这正是您所需要的。
唐·斯图尔特

你是对的!感谢您的澄清!Num下有什么类型的realToFrac无法使用?我不明白为什么它不是numToFrac。
jakebman

4
由于Num不提供任何转换功能,因此无法编写numToFrac。实数是我们拥有的最接近的东西(可以转换为Rational的Num类型),或者是整数(可以转换成无界整数的Num类型)。
唐·斯图尔特

谢谢!我被cs.ut.ee/~varmo/MFP2004/PreludeTour.pdf的最后一页上的图表误导了,该图表显示了Floating NOT继承Real的属性,然后我假定它们不会共享任何类型。至少我可以从错误中学习。
jakebman

25

Dons很好地回答了这个问题,我想我可以补充些内容。

以这种方式计算平均值时:

average xs = realToFrac (sum xs) / genericLength xs

您的代码将执行两次遍历列表,一次遍历列表元素的总和,一次遍历列表的长度。据我所知,GHC尚无法对其进行优化,无法单次计算总和和长度。

即使是初学者,也可以考虑使用它以及可能的解决方案,例如,平均函数可以使用计算总和和长度的折线编写;在ghci上:

:set -XBangPatterns

import Data.List

let avg l=let (t,n) = foldl' (\(!b,!c) a -> (a+b,c+1)) (0,0) l in realToFrac(t)/realToFrac(n)

avg ([1,2,3,4]::[Int])
2.5
avg ([1,2,3,4]::[Double])
2.5

该功能看起来不那么优雅,但是性能更好。

有关Dons博客的更多信息:

http://donsbot.wordpress.com/2008/06/04/haskell-as-fast-as-c-working-at-a-high-altitude-for-low-level-performance/


4
+1表示建议弃牌以获得更好的性能提升,但我只建议您在完全了解Haskell并且整数多态性适合您的情况下尝试性能。
罗伯特·马赛利

11
+1对Robert Massaioli注释,因为此avg实际上非常糟糕... foldl'在累加器中是严格的,对于Haskell而言,它基本上表示“弱头范式”,严格来说是第一个数据构造函数。因此,例如,在这里foldl'将保证对累加器进行足够的评估,以确定它是一对(第一个数据构造函数),但是不会对内容进行评估,因此会累加堆。在这种情况下,使用foldl' (\(!b,!c) a -> ...或严格对类型data P a = P !a !a是获得良好性能的关键。
Jedai 2012年

对于Jedai的评论+1,我对此帖子进行了相应的编辑。
David V.

9

由于dons在回答您的问题方面做得非常好,因此我将继续对您的问题进行提问。

例如,在您的问题中,您首先在给定列表上求平均值,得到了很好的答案。然后,您将看起来像完全相同的列表,将其分配给变量,然后使用该函数...然后将其炸掉。

你已经运行到这里是一个建立在编译器,称为DMR:在d readed 中号onomorphic [R estriction。当您将列表直接传递到函数中时,编译器无需假设数字是哪种类型,它只是根据使用情况推断出它可以是哪种类型,然后在无法将范围进一步缩小时选择一个。有点像鸭子打字的对立面。

无论如何,当您将列表分配给变量时,DMR就会启动。由于您已将列表放入变量中,但是没有给出有关如何使用列表的提示,因此DMR使编译器选择了一种类型。情况下,它选择了一个与表格相符并且看起来合适的:Integer。由于在您的函数不能使用整数/操作(它需要在一个类型Fractional类),它使这很抱怨:有没有实例IntegerFractional类。您可以在GHC中设置一些选项,以便在需要之前不会将您的值强制转换为单一形式(“单态”,是否得到它?),但这样会使任何错误消息的查找变得更加困难。

现在,在另一个音符上,您对Dons的回答得到了我的关注:

我被cs.ut.ee/~varmo/MFP2004/PreludeTour.pdf的最后一页上的图表误导了,该图表显示了Floating NOT继承Real的属性,然后我假定它们不会共享任何类型。

Haskell的类型与您习惯的类型不同。RealFloating类型类,它们比对象类更像接口。他们告诉您您可以使用该类中的类型做什么,但这并不意味着某些类型不能完成其他事情,只不过拥有一个接口就意味着一个(n OO风格的)类不能做其他事情还有其他的。

学习Haskell就像学习微积分

我想说学习Haskell就像学习瑞典语一样-许多小而简单的事物(字母,数字)在外观和工作上都是相同的,但是也有一些词看起来像是一件事,而实际上却是某事其他。但是,一旦您流利地使用它,您的普通朋友就会惊讶于您如何喷出这种奇怪的东西,从而使华丽的美女做出惊人的花样。奇怪的是,有很多人从一开始就参与Haskell,他们也了解瑞典语。也许那个比喻不仅仅是一个比喻。



-6

是的,Haskell的类型系统非常挑剔。这里的问题是fromIntegral的类型:

Prelude> :t fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b

fromIntegral将接受一个I​​ntegral,而不接受任何其他种类的Num。(/),仅接受小数。您如何才能使两者一起工作?

好,求和函数是一个不错的开始:

Prelude> :t sum
sum :: (Num a) => [a] -> a

Sum获取任何Num的列表并返回Num。

下一个问题是列表的长度。长度是一个整数:

Prelude> :t length
length :: [a] -> Int

您还需要将该Int转换为Num。这就是fromIntegral所做的。

因此,现在您有了一个返回Num的函数和另一个返回Num的函数。您可以查询一些用于数字类型推广的规则,但基本上,现在您可以开始了:

Prelude> let average xs = (sum xs) / (fromIntegral (length xs))
Prelude> :t average
average :: (Fractional a) => [a] -> a

让我们尝试一下:

Prelude> average [1,2,3,4,5]
3.0
Prelude> average [1.2,3.4,5.6,7.8,9.0]
5.4
Prelude> average [1.2,3,4.5,6,7.8,9]
5.25

11
您也迷上了与迈克尔相同的陷阱。数字重载!5不是整数值。它是任何数字。在这里,它默认为小数值-您不能传入Int或Integer。-您将不会获得(Fractional Int)的任何实例
Don Stewart 2010年

是的,我在那里不好。我没有给予足够的关注。我想这正是为什么我从未在Haskell中做过比玩具编程更多的事情的原因。即使是迷恋约束性和约束性语言的人,我也发现Haskell对高手有些野蛮。
我的正确观点

2
Haskell不是B&D。像B&D一样接近它会很痛苦。如果您想学习Haskell,则需要掌握类型系统。这里的所有都是它的。当您掌握如何使用类型时,您的困惑将消失。
nomen 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.