枚举列表时的奇数值

时间:2010-07-11 09:16:10

标签: haskell

作为更大函数定义的一部分,我需要允许函数的域(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?

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约定,表明它放弃了运行操作的结果,并注意到mapMforM确实收集了这些结果。

要通过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_putStrLnprintfnumeratordenominator

还有一些奇怪的东西。 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

如您所见,我们应用右手功能,然后将结果提供给左手功能。您可能还记得数学教科书中的定义,例如

rendered composition example

选择名称.是因为它在外观上与凸起的点相似。

因此,通过一系列功能组合,通过前后阅读来理解它通常是最简单的。

(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。