作为一项作业,我实现了以下sum
函数以返回数字列表的总和:
defmodule Homework do
@spec sum(list(number())) :: number()
def sum(l) do
case l do
[] -> 0
[a | as] -> a + sum(as)
end
end
end
作为单元测试,我使用了以下比较:
[-2, -2.1524700989447303, 1] |> fn(l) -> Enum.sum(l) === Homework.sum(l) end.()
此测试失败,返回false
。当我在iex
中运行函数时,得到以下结果,这对我来说是令人惊讶的:
iex(1)> [-2, -2.1524700989447303, 1] |> Enum.sum
-3.1524700989447307
iex(2)> [-2, -2.1524700989447303, 1] |> Homework.sum
-3.1524700989447303
此外,这两个函数始终一致地生成-3.1524700989447307
和-3.1524700989447303
。
为什么会发生这种差异?
修改
问题Why does changing the sum order returns a different result?为我指明了正确的方向,但是我认为这个问题的实际答案(OTP中的实现细节)也可能对某人很有趣。
答案 0 :(得分:3)
这个问题的答案Why does changing the sum order returns a different result?启发了我去参考源代码,以了解实现是如何实现的,当然,当参数为list
时,它将使用Erlang's implementation of foldl
,功能按 head + accumulator 的顺序而不是 accumulator + head 在我的实现中:
https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/enum.ex
@spec sum(t) :: number
def sum(enumerable) do
reduce(enumerable, 0, &+/2)
end
@spec reduce(t, any, (element, any -> any)) :: any
def reduce(enumerable, acc, fun) when is_list(enumerable) do
:lists.foldl(fun, acc, enumerable)
end
https://github.com/erlang/otp/blob/master/lib/stdlib/src/lists.erl
-spec foldl(Fun, Acc0, List) -> Acc1 when
Fun :: fun((Elem :: T, AccIn) -> AccOut),
Acc0 :: term(),
Acc1 :: term(),
AccIn :: term(),
AccOut :: term(),
List :: [T],
T :: term().
foldl(F, Accu, [Hd|Tail]) ->
foldl(F, F(Hd, Accu), Tail); %% here!
foldl(F, Accu, []) when is_function(F, 2) -> Accu.
修改
@Sneftel的评论使我进行了以下实验:
@spec sum(list(number())) :: number()
def sum(l) do
case Enum.reverse(l) do # reversing first
[] -> 0
[a | as] -> a + sum(as)
end
end
此新版本与Enum.sum
的结果相同:
iex(1)> Homework.sum([-2, -2.1524700989447303, 1])
-3.1524700989447307
iex(2)> Enum.sum([-2, -2.1524700989447303, 1])
-3.1524700989447307
所以看来这与订单有关。
编辑2
当列表不是相反顺序时,将a + sum(as)
更改为sum(as) + a
不会对结果造成任何影响。
def sum(l) do
case l do
[] -> 0
[a | as] -> sum(as) + a
end
end
iex(1)> Homework.sum([-2, -2.1524700989447303, 1])
-3.1524700989447303
iex(2)> Enum.sum([-2, -2.1524700989447303, 1])
-3.1524700989447307
因此,当我们谈论“顺序”的相关性时,它是folding发生的顺序,而不是操作数的顺序。
答案 1 :(得分:3)
我认为,这里的真正问题是,在处理浮点数时,永远不要期望做完全相等,因为它们本质上是不精确的。如果需要精度,则可以使用诸如Decimal
之类的模块。
defmodule Homework do
def sum([]), do: Decimal.new(0)
def sum([a | as]), do: Decimal.add(a, sum(as))
end
测试会话:
iex(1)> test = [Decimal.new(-2), Decimal.new("-2.1524700989447303"), Decimal.new(1)]
iex(2)> Homework.sum(test) === :lists.foldl(&Decimal.add/2, Decimal.new(0), test)
true
What Every Programmer Should Know About Floating-Point Arithmetic很好地概述了如何有效使用浮点数。