4

我正在尝试制作一个小函数来在具有给定增量的两个值之间进行插值。

[ 1.0 .. 0.5 .. 20.0 ]

编译器告诉我这已被弃用,并建议使用整数然后转换为浮点数。但是,如果我有一个分数增量,这似乎有点冗长 - 我是否必须将我的开始值和结束值除以我的增量,然后再乘以?(耶!)。

我曾经在某个地方看到过关于使用序列推导来做到这一点的东西,但我不记得是如何做到的。

请帮忙。

4

6 回答 6

9

TL;DR:F# PowerPack 的BigRational类型是要走的路


浮点循环有什么问题

正如许多人指出的那样,float值不适合循环:

  • 它们确实有舍入误差,就像1/3十进制一样,我们不可避免地会丢失从某个指数开始的所有数字;
  • 他们确实经历了灾难性的取消(当减去两个几乎相等的数字时,结果四舍五入为零);
  • 它们始终具有非零Machine epsilon,因此每次数学运算都会增加错误(除非我们多次添加不同的数字以使错误相互抵消——但循环并非如此);
  • 它们在整个范围内确实具有不同的精度:一个范围内的[0.0000001 .. 0.0000002]唯一值的数量等于 中的唯一值的数量[1000000 .. 2000000]

解决方案

可以立即解决上述问题的方法是切换回整数逻辑。

使用F# PowerPack,您可以使用BigRational以下类型:

open Microsoft.FSharp.Math
// [1 .. 1/3 .. 20]
[1N .. 1N/3N .. 20N]
|> List.map float
|> List.iter (printf "%f; ")

请注意,我冒昧地将步骤设置为,1/3因为0.5从您的问题实际上具有精确的二进制表示 0.1 b并表示为 +1.00000000000000000000000 * 2 -1;因此它不会产生任何累积求和误差。

输出:

1.000000; 1.333333;1.666667;2.000000; 2.333333; 2.666667; 3.000000; (跳过) 18.000000;18.333333; 18.666667; 19.000000; 19.333333; 19.666667; 20.000000;

// [0.2 .. 0.1 .. 3]
[1N/5N .. 1N/10N .. 3N]
|> List.map float
|> List.iter (printf "%f; ")

输出:

0.200000;0.300000;0.400000;0.500000;(跳过) 2.800000;2.900000;3.000000;

结论

  • BigRational使用整数计算,不比浮点慢;
  • 每个值仅发生一次舍入(转换为 afloat时,但不在循环内);
  • BigRational就好像机器 epsilon 为零;

有一个明显的限制:您不能使用无理数,例如pisqrt(2)因为它们没有精确的分数表示。这似乎不是一个很大的问题,因为通常情况下,我们不会同时循环有理数和无理数,例如[1 .. pi/2 .. 42]. 如果我们这样做(比如几何计算),通常有一种方法可以减少不合理部分,例如从弧度切换到度数。


进一步阅读:

于 2013-06-24T21:45:18.480 回答
5

有趣的是,浮动范围似乎不再被弃用。而且我记得最近看到一个问题(对不起,无法追踪)谈论浮动范围所体现的固有问题,例如

> let xl = [0.2 .. 0.1 .. 3.0];;

val xl : float list =
  [0.2; 0.3; 0.4; 0.5; 0.6; 0.7; 0.8; 0.9; 1.0; 1.1; 1.2; 1.3; 1.4; 1.5; 1.6;
   1.7; 1.8; 1.9; 2.0; 2.1; 2.2; 2.3; 2.4; 2.5; 2.6; 2.7; 2.8; 2.9]

我只是想指出,您可以在decimal类型上使用范围,而此类舍入问题要少得多,例如

> [0.2m .. 0.1m .. 3.0m];;
val it : decimal list =
  [0.2M; 0.3M; 0.4M; 0.5M; 0.6M; 0.7M; 0.8M; 0.9M; 1.0M; 1.1M; 1.2M; 1.3M;
   1.4M; 1.5M; 1.6M; 1.7M; 1.8M; 1.9M; 2.0M; 2.1M; 2.2M; 2.3M; 2.4M; 2.5M;
   2.6M; 2.7M; 2.8M; 2.9M; 3.0M]

如果你最后真的需要花车,那么你可以做类似的事情

> {0.2m .. 0.1m .. 3.0m} |> Seq.map float |> Seq.toList;;
val it : float list =
  [0.2; 0.3; 0.4; 0.5; 0.6; 0.7; 0.8; 0.9; 1.0; 1.1; 1.2; 1.3; 1.4; 1.5; 1.6;
   1.7; 1.8; 1.9; 2.0; 2.1; 2.2; 2.3; 2.4; 2.5; 2.6; 2.7; 2.8; 2.9; 3.0]
于 2011-04-16T23:10:29.863 回答
2

正如 Jon 和其他人所指出的,浮点范围表达式在数值上并不稳健。例如[0.0 .. 0.1 .. 0.3]等于[0.0 .. 0.1 .. 0.2]。在范围表达式中使用 Decimal 或 Int 类型可能会更好。

对于浮点数,我使用此函数,它首先通过最小的浮点步长将总范围增加 3 倍。我不确定这个算法现在是否非常健壮。但对我来说,确保停止值包含在 Seq 中就足够了:

let floatrange start step stop =
    if step = 0.0 then  failwith "stepsize cannot be zero"
    let range = stop - start 
                |> BitConverter.DoubleToInt64Bits 
                |> (+) 3L 
                |> BitConverter.Int64BitsToDouble
    let steps = range/step
    if steps < 0.0 then failwith "stop value cannot be reached"
    let rec frange (start, i, steps) =
        seq { if i <= steps then 
                yield start + i*step
                yield! frange (start, (i + 1.0), steps) }
    frange (start, 0.0, steps)
于 2013-06-23T16:06:20.303 回答
0

试试下面的序列表达式

seq { 2 .. 40 } |> Seq.map (fun x -> (float x) / 2.0)
于 2008-12-18T07:25:06.870 回答
0

您还可以编写一个相对简单的函数来生成范围:

let rec frange(from:float, by:float, tof:float) =
   seq { if (from < tof) then 
            yield from
            yield! frange(from + by, tof) }

使用它你可以写:

frange(1.0, 0.5, 20.0)
于 2008-12-18T11:50:32.450 回答
0

Tomas Petricek 的答案的更新版本,它编译并适用于递减范围(并且适用于测量单位):(但它看起来不那么漂亮)

let rec frange(from:float<'a>, by:float<'a>, tof:float<'a>) = 
   // (extra ' here for formatting)
   seq { 
        yield from
        if (float by > 0.) then
            if (from + by <= tof) then yield! frange(from + by, by, tof) 
        else   
            if (from + by >= tof) then yield! frange(from + by, by, tof) 
       }

#r "FSharp.Powerpack"
open Math.SI 
frange(1.0<m>, -0.5<m>, -2.1<m>)

更新我不知道这是否是新的,或者它是否总是可能的,但我刚刚发现(在这里),这 - 更简单的 - 语法也是可能的:

let dl = 9.5 / 11.
let min = 21.5 + dl
let max = 40.5 - dl

let a = [ for z in min .. dl .. max -> z ]
let b = a.Length

(注意,在这个特定的例子中有一个陷阱:)

于 2009-01-12T13:48:57.043 回答