作为更大函数定义的一部分,我需要允许函数的域(i,n)以不同的速率从i递增到n。所以我写道:
f (i, n) k = [i, (i+k)..n]
进入GHC。这返回了奇怪的结果:
*Main> f (0.0, 1.0) 0.1
[0.0,0.1,0.2,0.30000000000000004,0.4000000000000001,0.5000000000000001,0.6000000000000001,0.7000000000000001,0.8,0.9,1.0]
为什么GHC会返回,例如0.30000000000000004而不是0.3?
答案 0 :(得分:9)
因为IEEE浮点运算通常不能精确地表示十进制数。二进制表示中始终存在舍入误差,有时在显示数字时渗透到曲面。
根据GHC如何将浮点数转换为十进制表示,您可能会发现在Windows上它将结果显示为预期的0.3
。这是因为微软的运行时库比Linux和Mac更聪明,它们是如何呈现浮动的。
编辑:情况可能并非如此。使用IEEE浮点数时,数字0.3
将编码为整数3fd3333333333333
,而0.1 + 0.1 + 0.1
将生成编码为3fd3333333333334
的数字,我不知道Microsoft的运行时库可以容忍,在显示时可以回到0.3
。
无论如何,不同处理的一个很好的例子是将0.3
键入Python交互式shell。如果它是Python 2.6,你将返回0.29999999999999999
,如果它是2.7,它将显示0.3
。
答案 1 :(得分:4)
如果 i , n , k 是合理的,你可以去无限精度路线:
f :: (Rational, Rational) -> Rational -> [Rational]
f (i, n) k = [i, (i+k) .. n]
符号可能需要一点习惯:
ghci> f (0%1, 1%1) (1%10) [0 % 1,1 % 10,1 % 5,3 % 10,2 % 5,1 % 2,3 % 5,7 % 10,4 % 5,9 % 10,1 % 1]
将%
视为一个有趣的分数栏。
您可以使用
查看近似值import Control.Monad (mapM_)
import Data.Ratio (Rational, (%), denominator, numerator)
import Text.Printf (printf)
printApprox :: [Rational] -> IO ()
printApprox rs = do
mapM_ putRationalToOnePlaceLn rs
where putRationalToOnePlaceLn :: Rational -> IO ()
putRationalToOnePlaceLn r = do
let toOnePlace :: String
toOnePlace = printf "%.1f" (numFrac / denomFrac)
numFrac, denomFrac :: Double
numFrac = fromIntegral $ numerator r
denomFrac = fromIntegral $ denominator r
putStrLn toOnePlace
上面的代码是用完整类型注释的命令式样式编写的。读取其类型,将有理数列表转换为某些I / O操作。来自Control.Monad
的{{3}}组合器评估列表中每个值的动作(在这种情况下为putRationalToOnePlaceLn
)(我们想要近似的合理性)。您可以将其视为for
循环的类型,甚至还有一个mapM_
组合符与mapM_
相同,但参数的顺序是相反的。最后的下划线是一个Haskell约定,表明它放弃了运行操作的结果,并注意到mapM
和forM
确实收集了这些结果。
要通过forM_
安排近似值的输出,我们必须生成一个字符串。如果你用C语言写这个,你就会有
int numerator = 1, denominator = 10;
printf("%.1f\n", (double) numerator / (double) denominator);
上面的Haskell代码结构相似。 Haskell的/
运算符的类型是
(/) :: (Fractional a) => a -> a -> a
对于类型类a
的某些实例Fractional
,这说明当给出两个相同类型a
的值时,您将获得该类型的另一个值。
我们可以要求ghci
告诉我们Fractional
:
ghci> :info Fractional class (Num a) => Fractional a where (/) :: a -> a -> a recip :: a -> a fromRational :: Rational -> a -- Defined in GHC.Real instance Fractional Float -- Defined in GHC.Float instance Fractional Double -- Defined in GHC.Float
注意底部的instance
行。这意味着我们可以
ghci> (22::Float) / (7::Float) 3.142857
或
ghci> (22::Double) / (7::Double) 3.142857142857143
但不是
ghci> (22::Double) / (7::Float) <interactive>:1:16: Couldn't match expected type `Double' against inferred type `Float' In the second argument of `(/)', namely `(7 :: Float)' In the expression: (22 :: Double) / (7 :: Float) In the definition of `it': it = (22 :: Double) / (7 :: Float)
当然不是
ghci> (22::Integer) / (7::Integer) <interactive>:1:0: No instance for (Fractional Integer) arising from a use of `/' at :1:0-27 Possible fix: add an instance declaration for (Fractional Integer) In the expression: (22 :: Integer) / (7 :: Integer) In the definition of `it': it = (22 :: Integer) / (7 :: Integer)
请记住,Haskell的Rational
类型定义为Integers
的比率,因此您可以将putStrLn
视为C中的类型转换。
即使在阅读fromIntegral
之后,你仍然可能会发现Haskell对混合数字类型感到沮丧。对我们来说,在我们的头脑或纸上执行无限精度算术,忘记计算机只有有限的精度并且必须处理近似值,这太容易了。类型安全是一个有用的现实检查。
示例输出:
*Main> printApprox $ f (0%1, 1%1) (1%10) 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0
printApprox
的定义似乎对所有有用的路标感到安慰,例如函数名称和参数或类型注释。随着你对Haskell越来越有经验和舒适,这种看起来很拘谨的定义将开始变得杂乱无章。
Haskell是一种功能性语言:它的优势在于通过将简单函数组合成更复杂的函数来指定什么,而不是如何。有人曾经说过,Haskell操作函数的功能与Perl操纵字符串一样强大。
在A Gentle Introduction to Haskell: Numbers中,参数消失,留下计算的结构。学习阅读和这种风格确实需要练习,但你会发现它有助于编写更清晰的代码。
通过对导入的调整,我们可以定义一个无点等效项,例如
import Control.Arrow ((***), (&&&))
import Control.Monad (join, mapM_)
import Data.Ratio (Rational, (%), denominator, numerator)
import Text.Printf (printf)
printApproxPointFree :: [Rational] -> IO ()
printApproxPointFree =
mapM_ $
putStrLn .
toOnePlace .
uncurry (/) .
join (***) fromIntegral .
(numerator &&& denominator)
where toOnePlace = printf "%.1f" :: Double -> String
我们看到一些熟悉的内容:我们的新朋友mapM_
,putStrLn
,printf
,numerator
和denominator
。
还有一些奇怪的东西。 Haskell的$
运算符是另一种编写函数应用程序的方法。它的定义是
f $ x = f x
在您尝试
之前,这似乎并不十分有用Prelude> show 1.0 / 2.0 <interactive>:1:0: No instance for (Fractional String) arising from a use of `/' at :1:0-13 Possible fix: add an instance declaration for (Fractional String) In the expression: show 1.0 / 2.0 In the definition of `it': it = show 1.0 / 2.0
您可以将该行写为
show (1.0 / 2.0)
或
show $ 1.0 / 2.0
因此,您可以将$
视为编写括号的另一种方式。
然后有.
表示功能组合。它的定义是
(f . g) x = f (g x)
我们也可以写成
(f . g) x = f $ g x
如您所见,我们应用右手功能,然后将结果提供给左手功能。您可能还记得数学教科书中的定义,例如
选择名称.
是因为它在外观上与凸起的点相似。
因此,通过一系列功能组合,通过前后阅读来理解它通常是最简单的。
(numerator &&& denominator)
位使用Control.Arrow
中的point-free style。例如:
ghci> (numerator &&& denominator) $ 1%3 (1,3)
因此它将两个函数应用于相同的值,并返回一个包含结果的元组。请记住,我们需要将fromIntegral
应用于分子和分母,这就是join (***) fromIntegral
的作用。请注意,***
也来自Control.Arrow
模块。
最后,/
运算符采用单独的参数,而不是元组。迫切需要思考,你可能想写一些像
(fst tuple) / (snd tuple)
,其中
fst (a,_) = a
snd (_,b) = b
但在功能上思考!如果我们能以某种方式将/
转换为一个带有元组并使用其组件作为除法参数的函数,该怎么办?这正是fan-out combinator的作用!
你在Haskell迈出了第一步。享受旅程!
答案 2 :(得分:2)
更好的方法是采用
map (/10) [0 .. 10]
这需要整数,从而避免浮动问题,并将每一个除以10。