Comprehension syntax

I can do:

if true, do: :foo, else: :bar

and:

if true do
  :foo
else
  :bar
end

And I can do:

for i <- 0..3, do: {i, i+i}, into: %{}

but not:

for i <- 0..3 do
  {i, i+i}
into
  %{}
end

:thinking:

why is that?

1 Like

Because else got special treatment to ease user experience. This sugar is not available for any other atom outside the small set of predefined ones (else, rescue, catch, and after).

9 Likes

So I can’t use into, uniq, reduce in a for ... end block?

1 Like

You can, you have to pass them before the do.

for …, into: %{} do
  …
end
13 Likes
if true, do: :foo, else: :bar

and:

if true do
  :foo
else
  :bar
end

are different, although they look similar.

The first form is actually a if macro accepting two arguments.

if(true, [do: :foo, else: :bar])

And to make the second form works, we need a into macro which I don’t think we have it.

1 Like

That is not true. Both are exactly the same thing for the compiler:

iex(1)> quote do
...(1)> if true do
...(1)>   :foo
...(1)> else
...(1)>   :bar
...(1)> end
...(1)> end
{:if, [context: Elixir, import: Kernel], [true, [do: :foo, else: :bar]]}
iex(2)> quote do
...(2)> if(true, [do: :foo, else: :bar])
...(2)> end
{:if, [context: Elixir, import: Kernel], [true, [do: :foo, else: :bar]]}
3 Likes

Yes they are the same. A do block after a call adds the [do: ...] argument to the function/macro call on the left.

So if you call this (notice the parentheses for the if call)

if(a == 1) do
  :ok
end

The compiler kind of rewrites it to if(a == 1, [do: :ok])

This works for functions too:

defmodule Test do
  def myfun(value, block) do
    value |> IO.inspect(label: "value")
    block |> IO.inspect(label: "block")
  end
end

Test.myfun :some_value do
  a = 1
  b = 2
  a + b
end

This will output the following:

value: :some_value
block: [do: 3]

But as myfun/2 is a function and not a macro, the block is evaluated before being passed to the function, hence you see [do: 3], whereas with macros the quoted block is passed. This syntactic sugar is then less useful for functions.

The for macro is special, because if you pass into: %{}, do: stuff(), the do and into are in the same keyword list ; if you pass into: %{} and then a do end block, the compiler will add another argument to the for() call, containing just the do block:

iex(1)> quote do
...(1)> for a <- 1..2, into: %{} do
...(1)> {a, a*a} 
...(1)> end
...(1)> end
{:for, [],
 [
   {:<-, [],
    [{:a, [], Elixir}, {:.., [context: Elixir, import: Kernel], [1, 2]}]},
   [into: {:%{}, [], []}],
   [
     do: {{:a, [], Elixir},
      {:*, [context: Elixir, import: Kernel],
       [{:a, [], Elixir}, {:a, [], Elixir}]}}
   ]
 ]}
iex(2)> quote do                   
...(2)> for a <- 1..2, into: %{}, do: {a, a*a}
...(2)> end
{:for, [],
 [
   {:<-, [],
    [{:a, [], Elixir}, {:.., [context: Elixir, import: Kernel], [1, 2]}]},
   [
     into: {:%{}, [], []},
     do: {{:a, [], Elixir},
      {:*, [context: Elixir, import: Kernel],
       [{:a, [], Elixir}, {:a, [], Elixir}]}}
   ]
 ]}

I guess that is why for is defined in Kernel.SpecialForms as an error because it is handled at the compiler level.

1 Like

Well, no. It is still consistent behaviour among all other calls:

quote do
  foo bar: 1, do: 10
end
# => {:foo, [], [[bar: 1, do: 10]]}

quote do
  foo bar: 1 do
    10
  end
end
# => {:foo, [], [[bar: 1], [do: 10]]}

There are 2 reasons why for is special form and not “regular” macro:

  • Compiler need to know that it is comprehension, so it can output Erlang comprehension as well
  • <<a <- binary>> syntax need special handling
  • Macros do not support variable arguments list (the same rules as any other call apply there as well)

So all of the above forces the Elixir to treat for as a “special thing” within parser itself.

I don’t understand to what you say “no”.

I mean in your example it calls two different foo functions. The for construct handles that in a special way, there are not several definitions for for to my knowledge in the Elixir code.

As I said, it is literally impossible to define for in Elixir. I meant that do: 10 and do … end aren’t really treated differently just for for, but the same rules applies to all calls.

for is special form not because of do but because each x <- y is separate argument, so for i <- 1..10, do: i is 2-ary function and for i <- 1..10, j <- 1..10, do: i + j is 3-ary function. So either for would need to have N different heads and just fail in case if someone would like to use more than that, then it would fail (see older Rust compilers to see that problem, there Debug is defined only up to 32-ary tuples, as there is no variadic generics). Due that problem for and with must be defined as special forms, because they require parser magic to be then expanded properly.

2 Likes

Ok so it is not defined as a special form just for the arity problem, but otherwise it is just what I said, basically it is a special form because it requires special treatment at the compiler level.