Elixir equivalent of the spread operator?

How do I use an arbitrary number of arguments in an anonymous function?
Ex. in JavaScript
…args
Ex. in Python
*args

I need to be able to take any number of arguments in and convert them into a list with an anonymous function.

2 Likes

Arity is fixed. So you have to pass a list to begin with. Often keyword lists are used.

Examples:

Kernel.spawn/3 - the third argument args is a list of indeterminate length.
Supervisor.start_link/3 where options is a keyword list.

The List module has some special functions like List.keyfind/4 that can be used with keyword lists.

Map.new/1 converts a keyword list to a map - though when a key is duplicated only one key-value is kept.

To ‘spread’ you have to call apply:

╰─➤  iex
Erlang/OTP 20 [erts-9.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.6.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> blah = fn(a, b, c, d) -> a+b+c+d end
#Function<4.99386804/4 in :erl_eval.expr/5>
iex(2)> args = [1,2,3,4]       
[1, 2, 3, 4]
iex(3)> apply(blah, args)
10
iex(4)> defmodule Bloop do def bleep(a, b, c, d), do: a+b+c+d end
{:module, Bloop,
 <<70, 79, 82, 49, 0, 0, 4, 32, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 116,
   0, 0, 0, 13, 12, 69, 108, 105, 120, 105, 114, 46, 66, 108, 111, 111, 112, 8,
   95, 95, 105, 110, 102, 111, 95, 95, 9, ...>>, {:bleep, 4}}
iex(5)> apply(Bloop, :bleep, args)
10
iex(6)> apply(Bloop, :bleep, [0 | args])
** (UndefinedFunctionError) function Bloop.bleep/5 is undefined or private. Did you mean one of:

      * bleep/4

    Bloop.bleep(0, 1, 2, 3, 4)

Do note, this is an indirect call so although still cheap enough, don’t call it in a tight loop where performance is a top necessity, but otherwise it’s perfectly fine to use. :slight_smile:

For that you’ll need a macro. Functions on the BEAM are like functions in C++ or so, they are defined by a name and arity, thus the arity has to match to be called. You can generate many functions that take each count of args and return that, but a macro can do it inline, however you cannot make that anonymous for obvious reasons (ran at compile-time, not run-time). ^.^

To take an arbitrary number of arguments that are not in the ‘arity’ you should pass in a list, or map, or whatever structure is appropriate. :slight_smile:

9 Likes

I never got an answer that I could use on this, but I’m still very new to Elixir. I was trying to add a sort value to an existing list of maps. I ended up just passing the list down to my client and added it using the javascript function below, but I’d love to know how Elixir handles this?

sortResponders(auction){
return auction.responders.map((r, index) => ({…r, sort: index + 1 }));
}

The BEAM does not support variable arity functions. So you have to use lists or the like

1 Like

Thanks zkessin…I figured I just need to get stronger on lists, maps, etc. The js workaround will do until I get there.

{obj..., a: 1, b: 2, c: 3}

  • If the map already has a :sort atom key you could simply use the %{r | sort: index + 1} update syntax sugar; that syntax can accomodate multiple key updates %{map | a: 1, b: 2, c: 3}

  • If the key may not exist use Map.put/2 - Map.put(map, :b, 2), for multiple keys you can use Map.merge/2 - Map.merge(map,%{a: 1, b: 2, c: 3})

const {a, b, c, ...rest} = obj

iex(1)> map = %{a: 1, b: 2, c: 3, d: 4}
%{a: 1, b: 2, c: 3, d: 4} 
iex(2)> isolate = [:a, :b, :c]
[:a, :b, :c]
iex(3)> {%{a: a, b: b, c: c}, rest} = Map.split(map, isolate)
{%{a: 1, b: 2, c: 3}, %{d: 4}}
iex(4)> IO.puts("#{inspect a} #{inspect b} #{inspect c}  #{inspect rest}")
1 2 3  %{d: 4}
:ok
iex(5)> taken = Map.take(map, isolate)
%{a: 1, b: 2, c: 3}
iex(6)> IO.puts("#{inspect taken}")
%{a: 1, b: 2, c: 3}
:ok
iex(7)> other = Map.delete(map, :a)
%{b: 2, c: 3, d: 4}
iex(8)> IO.puts("#{inspect other}")
%{b: 2, c: 3, d: 4}
:ok
iex(9)> left = Map.drop(map, isolate)
%{d: 4}
iex(10)> IO.puts("#{inspect left}")
%{d: 4}
:ok
iex(11)> 
1 Like

Thanks peerreynders, but like I said, I need to go back through my books/courses and work on lists. I’m assuming I would add that inside my Enum.map function to update my list? My sample data structure is as follows (I want dave to have sort: 1, and pete to have sort: 2) :

 iex(1)> r_list = [%{first_name: "dave", last_name: "boo", sort: 9999}, %{first_name: "pete", last_name: "street", sort: 9999}]

In your code {...r, sort: index + 1} is equivalent to Map.put(r, :sort, index + 1).

In Elixir you don’t need the spread operator to make copy, because variables are immutable.

You can use the mentioned construct of %{r | sort: index + 1} if you are sure that key sort exists in r. Map.put will create a key if it does not exist.

However, the outer map of responders.map((r, index) => ({…r, sort: index + 1 })) will not work without a little help. If you use Enum.map you pass a function of single argument into it. It won’t receive the index of item. You must zip the values with indices first i.e. using Enum.with_index, but remember that since now you will have list of two element tuples - {value, index}.

So

responders.map((r, index) => ({…r, sort: index + 1 }))

can be written as

responders
|> Enum.with_index(1)
|> Enum.map(fn {r, index} -> Map.put(r, :sort, index) end)

More info and great examples you can find in Enum documentation.

2 Likes

Enum.map/1 doesn’t supply an index - that is a JavaScript Array thing.

defmodule Demo do
  #
  # version using comprehension
  # https://elixir-lang.org/getting-started/comprehensions.html
  def adjust_sort(list, offset \\ 0),
    do: for({m, index} <- Enum.with_index(list, offset), do: %{m | sort: index})

  #
  # version using recursion
  def adjust_sort2(list, offset \\ 0),
    do: adjust_sort(list, offset, [])

  # base/terminal case. Processed entire list. Reverse it to get original order
  defp adjust_sort([], _, list),
    do: :lists.reverse(list)

  # http://erlang.org/doc/man/lists.html#reverse-1

  # recursive case. Update sort value on this element, recurse on next element
  defp adjust_sort([h | t], index, rest),
    do: adjust_sort(t, index + 1, [%{h | sort: index} | rest])

  #
  # version using reduce/foldl
  def adjust_sort3(list, offset \\ 0) do
    {result, _} = List.foldl(list, {[], offset}, &reducer/2)
    :lists.reverse(result)
  end

  defp reducer(m, {rest, index}),
    do: {[%{m | sort: index} | rest], index + 1}

  #
  # version using Enum.map_reduce
  def adjust_sort4(list, offset \\ 0) do
    {result, _} = Enum.map_reduce(list, offset, &map_reducer/2)
    result
  end

  defp map_reducer(m, index),
    do: {%{m | sort: index}, index + 1}
end

r_list = [
  %{first_name: "dave", last_name: "boo", sort: 9999},
  %{first_name: "pete", last_name: "street", sort: 9999}
]

IO.inspect(Demo.adjust_sort(r_list, 1))
IO.inspect(Demo.adjust_sort2(r_list, 1))
IO.inspect(Demo.adjust_sort3(r_list, 1))
IO.inspect(Demo.adjust_sort4(r_list, 1))
$ elixir demo.exs
[
  %{first_name: "dave", last_name: "boo", sort: 1},
  %{first_name: "pete", last_name: "street", sort: 2}
]
[
  %{first_name: "dave", last_name: "boo", sort: 1},
  %{first_name: "pete", last_name: "street", sort: 2}
]
[
  %{first_name: "dave", last_name: "boo", sort: 1},
  %{first_name: "pete", last_name: "street", sort: 2}
]
[
  %{first_name: "dave", last_name: "boo", sort: 1},
  %{first_name: "pete", last_name: "street", sort: 2}
]
$

I think the Enum.map_reduce/3 version is the closest in spirit for what you are looking for.

1 Like

Yes, the documentation led me down the Enum.with_index path, but the tuples tripped me up. I ran into time constraints, and went for the workaround. Thanks, and I hope my newbness helps others reading your suggestions.

Yes, now that I’ve had sleep and coffee, map_reduce/3 looks like something I could make work. :slight_smile: