Reputation: 8079
As a homework, I've implemented the following sum
function to return the sum of a list of numbers:
defmodule Homework do
@spec sum(list(number())) :: number()
def sum(l) do
case l do
[] -> 0
[a | as] -> a + sum(as)
end
end
end
And as a unit test I've used the following comparison:
[-2, -2.1524700989447303, 1] |> fn(l) -> Enum.sum(l) === Homework.sum(l) end.()
And this test fails, returning false
. When I ran the functions in iex
I got the following result, which is surprising for me:
iex(1)> [-2, -2.1524700989447303, 1] |> Enum.sum
-3.1524700989447307
iex(2)> [-2, -2.1524700989447303, 1] |> Homework.sum
-3.1524700989447303
What is more, both functions consistently generate -3.1524700989447307
and -3.1524700989447303
, respectively.
Why does this difference happens?
Edit
The question Why does changing the sum order returns a different result? pointed me in the right direction but I think that the actual answer to this question (an implementation detail in OTP) could be interesting to someone as well.
Upvotes: 3
Views: 385
Reputation: 23129
I think the real issue here is that one should never expect to do exact equality when working with floating point numbers, because they are inherently imprecise. If precision is needed, then a module such as Decimal
can be used.
defmodule Homework do
def sum([]), do: Decimal.new(0)
def sum([a | as]), do: Decimal.add(a, sum(as))
end
Test session:
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 provides a nice overview of how to effectively work with floating point numbers.
Upvotes: 3
Reputation: 8079
The answer to this question Why does changing the sum order returns a different result? inspired me go to the source to see how the implementation was and, surely enough, when the parameter is a list
it uses Erlang's implementation of foldl
, which applies the function in the order head + accumulator instead of accumulator + head like in my implementation:
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.
Edit
@Sneftel's comment made me do the following experiment:
@spec sum(list(number())) :: number()
def sum(l) do
case Enum.reverse(l) do # reversing first
[] -> 0
[a | as] -> a + sum(as)
end
end
This new version has the same result as Enum.sum
:
iex(1)> Homework.sum([-2, -2.1524700989447303, 1])
-3.1524700989447307
iex(2)> Enum.sum([-2, -2.1524700989447303, 1])
-3.1524700989447307
So it seems like it is about the order.
Edit 2
Changing a + sum(as)
to sum(as) + a
makes no difference in the result when the list not in reverse order.
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
So when we talk about the relevance of 'order' it is the order in which folding is happening, not the order of the operands.
Upvotes: 3