Randy Canegaly
Randy Canegaly

Reputation: 39

Why does my Enum.reduce based implementation of Enum.all? return an empty list?

The Elixir help page for Enum.reduce/3 says that almost all of the Enum functions can be implemented on top of Enum.reduce/3. I am trying to implement Enum.all?/1 by writing a new function that uses Enum.reduce/3. It works correctly most of the time but in some cases it returns an empty list.

In one case I have invoked the function passing a 1..7 range and a function testing that a value is less than 7. In another case I passed the 1..7 range and a function testing that a value is less than 8.

In the first case my all?/1 function correctly returns false as all values are not less than 7.

In the second case I am expecting true, all values are less than 8 but instead get back an empty list.

Here is my implementation of the all?/1 function .....

defmodule ListsRec5 do

  def all?(enumerable, acc, fun \\fn x -> x end) do
    enumerable
      |> Enum.reduce([], fn x, acc -> fun.(x) and acc end)
  end

end

The first test......

ListsRec5.all?(1..7, true, fn x -> x < 7 end)
false

The second test ....

ListsRec5.all?(1..7, true, fn x -> x < 8 end)
[]

I think the second test should return true, not an empty list.

Upvotes: 1

Views: 1001

Answers (3)

Fredy M
Fredy M

Reputation: 56

The issue here is that you are using incorrectly the accumulator passed to reduce/3

  def all?(enumerable, fun \\fn x -> x end) do
    enumerable
      |> Enum.reduce(true, fn x, acc -> fun.(x) and acc end)
  end

The arity of all? function is 2 (not 3)

The initial value passed to the reduce function should be true

Upvotes: 0

7stud
7stud

Reputation: 48659

The second test .... ListsRec5.all?(1..7, true, fn x -> x < 8 end) []

I think the second test should return true, not an empty list.

Well, let's see:

iex(3)> true and []  
[]
iex(4)> true and []
[]
iex(5)> true and []
[]
iex(6)> true and []
[]
iex(7)> true and []
[]
iex(8)> true and []
[]
iex(9)> true and []
[]

Yup, that's an empty list. I've read that:

  1. and requires boolean arguments and returns a boolean.

  2. and requires the first argument to be a boolean and returns a boolean.

The example above disproves both of those incompetent claims. So, lets ignore the Elixir writer's vain attempts to explain how and works because apparently and in Elixir is equivalent to andalso in Erlang. So let's check the Erlang docs:

 Expr1 andalso Expr2

Returns either the value of Expr1 (false) or the value of Expr2 (if Expr2 is evaluated).

So, if Expr1 is true, then andalso returns Expr2, otherwise andalso returns false, i.e. when Expr1 is false.

From Erlang/OTP R13A, Expr2 is no longer required to evaluate to a Boolean value.

That explains why you are getting an empty list:

~/erlang_programs$ erl
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V9.3  (abort with ^G)

1> true andalso false.
false

2> true andalso [].
[]

3> true andalso 10.
10

4> false andalso "hello".
false

Also notice here:

  def all?(enumerable, acc, fun \\fn x -> x end) do
    enumerable
    |> Enum.reduce([], fn x, acc -> fun.(x) and acc end)
  end

that the acc variable in the all? def's parameter list is unused. The function should be defined like this:

   def all?(enumerable, acc, fun \\fn x -> x end) do
     enumerable
     |> Enum.reduce(acc, fn x, curr_acc -> fun.(x) and curr_acc end)
   end

Then you can call it like this:

~/elixir_programs$ iex a.ex
Erlang/OTP 20 [erts-9.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.8.2) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> A.all?(1..7, true, fn x -> x<8 end)
true

iex(2)> A.all?(1..7, true, fn x -> x<7 end)
false

Somewhere in the looping you will get acc=true and false, which returns false, and thereafter false and anything will return false. As a result, if the predicate function returns false for any element in the enumerable, then the end result will be false.

Upvotes: -1

Brett Beatty
Brett Beatty

Reputation: 6003

If you're reducing into a boolean (reduce won't be the best approach here, but it can be done), your accumulator is going to be a boolean, not a list, and each iteration will be a boolean representing "everything prior was true and this one is true".

Enum.reduce(enumerable, true, fn x, acc -> acc and fun.(x) end)

Upvotes: 2

Related Questions