Blanen
Blanen

Reputation: 782

Function not defined during piping, except when returning an anonymous function

Probably a very basic Elixir question,

I want to sum the even number from a 1 to 10 and output it to IO.puts

First I try doing it this way:

1..10
|> Enum.filter(fn (x) -> rem(x, 2) == 0 end)
|> Enum.sum
|> IO.puts

Which works as expected.


Then I try defining that function inside a module as such:

defmodule Test do
  def is_even(x) do
    rem(x, 2) == 0
  end
end

1..10
|> Enum.filter(Test.is_even)
|> Enum.sum
|> IO.puts

But this gives me the following error on compilation:

** (UndefinedFunctionError) undefined function: Test.is_even/0
    Test.is_even()
    tmp/src.exs:8: (file)
    (elixir) src/elixir_lexical.erl:17: :elixir_lexical.run/3
    (elixir) lib/code.ex:316: Code.require_file/2

Why is it looking for is_even/0 when it should( in my intention) be looking for is_even/1?


I don't understand why this is, especially since doing this:

defmodule Test do
  def hello(x) do
    IO.puts(x)
  end
end

Test.hello("Hello World!")

Works completely fine.


I also just found out this works:

defmodule Test do
  def is_even() do
    fn (x) -> rem(x, 2) == 0 end
  end
end

1..10
|> Enum.filter(Test.is_even)
|> Enum.sum
|> IO.puts

Why is it using the return of the function to use as a function instead of using the function itself?

Is there a way to make this work without having to return anonymous functions inside functions?

Upvotes: 2

Views: 359

Answers (3)

Stratus3D
Stratus3D

Reputation: 4906

Yes, what you want to do is possible and easy. You are trying to pass a function around as you would other values in Elixir, which is possible. For example:

f = &IO.puts/1

This assigns IO.puts/1 to the variable f, which can be used like other values. Whenever referencing a function like this you must include the arity of the function, which in your case is 1 because your function takes one argument. When calling functions the arity is implicit because you are passing in a number of arguments, but when you are referencing a function this way you must specify the arity manually because there could be multiple versions of your is_even function with different arities and the VM won't know which one you want.

You code should look like this:

defmodule Test do
  def is_even(x) do
    rem(x, 2) == 0
  end
end

1..10
|> Enum.filter(&Test.is_even/1)
|> Enum.sum
|> IO.puts

Upvotes: 1

Ricardo Marinovic
Ricardo Marinovic

Reputation: 206

defmodule Test do
  def is_even(x) do
    rem(x, 2) == 0
  end
end

1..10
|> Enum.filter(Test.is_even)
|> Enum.sum
|> IO.puts

|> Enum.filter(Test.is_even) is the line that doesn't work because of how the pipe operator works. The pipe operator will put the result of the expression before it (1..10) as the first argument for Enum.filter/2, but it won't put it as an argument for Test.is_even/1 since it is inside filter.

That line is like writing: Enum.filter 1..10, Test.is_even(), no argument is passed for Test.is_even, so it looks for a 0-arity function but it couldn't find one.

The is_even/0 on your last example works because that function returns an anonymous function which is the same as the first example you gave.

Upvotes: 1

Justin Wood
Justin Wood

Reputation: 10051

Unlike some common languages, Elixir (and Erlang and probably all BEAM based languages) allow you to define multiple functions with the same name. Elixir looks to try and match the function call with a definition that has the same arity that you called it with.

Arity is the number of arguments a function takes. For example, the following function has an arity of 0

def foo() do
  IO.puts "foo"
end

while this function

def bar(x) do
  IO.puts "bar"
end

has an arity of 1, even though the argument goes unused.

With your module

defmodule Test do
  def is_even(x) do
    rem(x, 2) == 0
  end
end

You have defined the function is_even with an arity of 1.

When you tried to call your function Test.is_even in your chain, you called it as if it had an arity of 0. If you want to specify that your function is expected to be of arity 1, you can use the following notation, &Test.is_even/1. So your example would become

1..10
|> Enum.filter(&Test.is_even/1)
|> Enum.sum
|> IO.puts

Upvotes: 3

Related Questions