Anonymous Function -VS- Capture Functions

Hello.

I have piece of code that creates a permutation of a list. I first implemented this using an example I found that uses list comprehensions. This version works, but I wanted to really understand how this works under the hood. I implemented a second version that uses Enum.map and Enum.concat that produces equivalent output to the list comprehension version. However, I wasn’t happy with this version because to make it work, I have to implement two named functions to handle the permutation. I attempted (many times) to create a version that uses only one named function to produce the desired output and always run into problems. The specific problem I am having now is that it seems anonymous functions, as passed as an argument to Enum.map do not receive the second argument. However, if I use basically the same code but with a capture to the named function, instead of the anonymous function, the second argument gets passed along. I find this behavior confounding and am hopeful that someone will be able to help me understand it. Annotated code examples below.

Thanks!

  def permute([]), do: [[]]

  # List Comprehension version
  def permute(list) do
    for x <- list, y <- permute(list -- [x]) do [x|y] end
  end

  # Equivalent to List Comprehension version, only using Enum.map and Enum.concat
  def permute(list) do
    Enum.map(list, &(permute_inner(&1, list)))
    |> Enum.concat()
  end

  defp permute_inner(x, list) do
    permute(list -- [x])
    |> Enum.map(&([x|&1]))
  end

  # Logically equivalent to Enum.map variant above, removes the need for inner_permute.
  # Compiles, but list does not get passed in.
  def permute(list) do
    Enum.map(list, fn(x, list) -> permute(list -- [x]) |> Enum.map(&([x|&1])) end)
    |> Enum.concat()
  end
1 Like

In the last function you’re passing a function that takes two arguments to Enum.map. Enum.map calls its mapping function only with the elements of the enumerable that is passed as its first argument. In effect, you’re referring to another list (the parameter) inside that function than the list that you think is being passed as the second argument to the map function. I’m not even sure why it compiles/runs, just tested it in iex and it fails trying to call a two arity function with one argument.

2 Likes

In code

defmodule Demo do
  def permute([]) do
    [[]]
  end

  def permute(list) do
    for x <- list, y <- permute(list -- [x]) do [x|y] end
  end

  def permute_m([]) do
    [[]]
  end

  def permute_m(list) do
    list
    |> Enum.map(
      fn(x) ->       # Removed list parameter HERE
        (list -- [x])
        |> permute_m()
        |> Enum.map(&([x|&1]))
      end
      )
    |> Enum.concat()
  end
end

my_list = [1,2,3,4]
IO.inspect(Demo.permute(my_list))
IO.inspect(Demo.permute_m(my_list))
$ elixir test.exs
[[1, 2, 3, 4], [1, 2, 4, 3], [1, 3, 2, 4], [1, 3, 4, 2], [1, 4, 2, 3],
 [1, 4, 3, 2], [2, 1, 3, 4], [2, 1, 4, 3], [2, 3, 1, 4], [2, 3, 4, 1],
 [2, 4, 1, 3], [2, 4, 3, 1], [3, 1, 2, 4], [3, 1, 4, 2], [3, 2, 1, 4],
 [3, 2, 4, 1], [3, 4, 1, 2], [3, 4, 2, 1], [4, 1, 2, 3], [4, 1, 3, 2],
 [4, 2, 1, 3], [4, 2, 3, 1], [4, 3, 1, 2], [4, 3, 2, 1]]
[[1, 2, 3, 4], [1, 2, 4, 3], [1, 3, 2, 4], [1, 3, 4, 2], [1, 4, 2, 3],
 [1, 4, 3, 2], [2, 1, 3, 4], [2, 1, 4, 3], [2, 3, 1, 4], [2, 3, 4, 1],
 [2, 4, 1, 3], [2, 4, 3, 1], [3, 1, 2, 4], [3, 1, 4, 2], [3, 2, 1, 4],
 [3, 2, 4, 1], [3, 4, 1, 2], [3, 4, 2, 1], [4, 1, 2, 3], [4, 1, 3, 2],
 [4, 2, 1, 3], [4, 2, 3, 1], [4, 3, 1, 2], [4, 3, 2, 1]]

The effect is usually referred to as variable shadowing.

The list in the working version can be accessed directly because it is part of the anonymous function’s closure, i.e. the scope that created the anonymous function remains accessible to it.

2 Likes

Is this generally the case for all anonymous functions and capture functions? Are capture functions distinctly different from anonymous functions in this regard? I don’t see that reflected in any docs on capture functions that they have access to their enclosing scope, while anonymous functions do not. Or maybe I missed something. Anyway, thanks for having a look. I still want to try to figure out how to get this down to a single named function without the list comprehensions and without a macro…

1 Like

Yes, I figured this is what is happening, just don’t know how to fix it. :slight_smile: Thanks for having a look.

1 Like

Capture functions are syntactic sugar around anonymous functions.

As @peerreynders noted, your second version is not logically equivalent:

&(permute_inner(&1, list)))
is the same as:
fn arg1 -> permute_inner(arg1, list) end
Which is very different from:
fn arg1, list -> permute_inner(arg1, list) end

In the first two cases, list is grabbed from the outer context. In the last case, list is expected as parameter. Even while it compiles, it breaks at runtime with a BadArity error, since you are passing an arity-2 function into Enum.map which expects an arity-1 function.

3 Likes

OMG. Sometimes I just feel dumb. Thank you for that. :joy:

1 Like

For those interested, these are equivalent, both work with just the one function:

  def permute([]), do: [[]]

  # List Comprehension version
  def permute(list) do
    for x <- list, y <- permute(list -- [x]) do [x|y] end
  end

  # Enum.map variant
  def permute(list) do
    Enum.map(list, fn(x) -> permute(list -- [x]) |> Enum.map(&([x|&1])) end)
    |> Enum.concat()
  end
1 Like
  1. You can use Enum.flat_map/2, eliminating the need for concat after map.
 def permute_rest_fn(list),
    do:
      fn(x) ->
        (list -- [x])
        |> permute()
        |> Enum.map(&([x|&1]))
      end

  def permute([]),
    do: [[]]
  def permute(list),
    do: Enum.flat_map(list, permute_rest_fn(list))

Fewer functions aren’t necessarily an improvement as the function name can be used in a more declarative manner to explain what you are doing rather than (anonymously) simply focusing on how you are doing it.

  1. Have a look at the pipeline style guide.

  2. Please use fenced code blocks for source code (some of the languages mentioned here and here have support for syntax highlighting).

Thank You!

2 Likes