thiagomajesk

thiagomajesk

Just to expand on this a little more… I personally don’t think that the correlation would be easier for beginners because guards syntax is already well-defined in elixir by using the when keyword. Also, is not usual to use guards with functions that don’t return a boolean value (if I’m not mistaken, but I might be wrong on this). So, it seems that instead of “similar to guards”, it’s just ‘qualifiers’ in a different position.

Could you elaborate on other use cases you’ve thought would be useful (similarly to the proposed usage, at least)? One of the concerns brought up in previous discussions was that ‘qualifiers’ were strange and specific to for and not commonly seen in other parts of the language like I said.

0
Post #92
derekbrown

derekbrown

Guards are a qualifier in the position that I’m referencing. That’s the analog.

Could you elaborate on other use cases you’ve thought would be useful (similarly to the proposed usage, at least)?

One example, when piping into conditionals like case/cond, it could be nice to have additional data there with the conditional itself, rather than “init-ing” values above the pipe chain. Could possibly also be used in the function head as a more explicit default for recursive functions.

0
Post #93
brettbeatty

brettbeatty

That’s something I’ve thought would be useful to add to for comprehensions (I actually hacked on a macro for this recently). In my mind it makes comprehensions more composable–you can pass the comprehension streams into additional comprehensions or Enum/Stream functions without many traversals of your list. I see stream comprehensions as the Ecto.Query.from/2 macro if the Stream module is the rest of Ecto.Query (hopefully that makes sense).

I think in this discussion folks are more looking for a way to reduce without leaving for, but I like the idea of being able to mix & match.

1
Post #94
stefanchrobot

stefanchrobot

Agreed on this a lot! I particularly don’t like how Ecto’s Repo.transaction changes behaviour based on whether you’re passing a function or a Multi. Unless there’s something I’m missing, there should be Multi.transaction(multi) instead of Repo.transaction(multi) (or maybe Multi.run to mirror Stream.run except that Multi.run is already a thing).

I’d prefer for let and even forlet instead of for_let. While those are all macros or special forms, there are some constructs that are treated as “keywords” (defmodule, for, while, if, etc.). Somehow for_let doesn’t fit in there.

But… another approach to solving this would be to declare the shape of the returned value in the for’s “header”. Also, thinking about this more, reduce is not friendly to people coming from imperative languages. I think talking about the “returned value” is way more familiar, so I’m also proposing to get rid of reduce altogether in favour of return.

So a map-reduce would be:

for return {sum = 0, count = 0, i <- [1, 2, 3]} do
  sum = sum + 1
  count = count + 1
  {sum, count, i}
end

and reduce:

for return {sum = 0, count = 0}, i <- [1, 2, 3] do
  sum = sum + 1
  count = count + 1
  {sum, count}
end
3
Post #95
josevalim

josevalim OP

Creator of Elixir

I have considered this route but I can’t come up with any reasonable syntax. Putting the generator as part of the return type feels incorrect, the generator is not really part of the result and you can have multiple generators, which would not make sense either. We would need a way to refer to the output but all of them would feel magical.

2
Post #96
stefanchrobot

stefanchrobot

I think that the following would make sense, except that I’m not sure what should be done with pattern matching.

for i <- [1, 2, 3], return {sum = 0, count = 0, i} do
  sum = sum + 1
  count = count + 1
  {sum, count, i}
end

Flipping the order maybe would solve that by “enforcing” to bind to a variable:

for return {sum = 0, count = 0, i}, <some pattern> = i <- [1, 2, 3],  do
  sum = sum + 1
  count = count + 1
  {sum, count, i}
end

Did you consider a return-like macro inside the block? I know that some options have been dismissed on the mailing list due to “refactorability” of the code, but to me this seems similar to rolling back Ecto transactions - there is some minimal amount of plumbing/wiring needed:

Repo.transaction(fn ->
  # ...
  # do stuff, but at some point:
  |> case do
    {:ok, foo} -> foo
    {:error, reason} -> Repo.rollback(reason)
  end
end)

So how about something like:

for i <- [1, 2, 3], sum = 0, count = 0 do
  sum = sum + 1
  count = count + 1
  continue {i, sum, count}
end

I guess I’m starting to lean towards something like after from the initial proposal, but maybe more of a single expression than imperative rebindings.

EDIT: after would work too, but continue would be best for people from imperative background:

for i <- [1, 2, 3], sum = 0, count = 0 do
  sum = sum + 1
  count = count + 1
after
  {i, sum, count}
end

If we had continue I’d love to see break for early exits.

0
Post #97
fidr

fidr

I feel like for is already an odd construct, because it behaves like a Enum.map (with nesting and filtering). Ideally I think you’d want generators to be more integrated with the standard library, but that’s hard to do right now.

To avoid confusion, I would let the for contruct mirror the functions we know as much as possible, instead of making up new terms for the same functions we already have in Enum.

Something like this (map can be the default unless specified):

for map x <- [1,2,3], y <- [4,5,6] do
  {x, y}
end

for map_reduce acc = 0, x <- [1,2,3], y <- [4,5,6] do
  {{x, y}, acc + x + y}
end

for reduce acc = 0, x <- [1,2,3], y <- [4,5,6] do
  acc + x + y
end

This will also leave some room for possible future expansion for any other enumerable functions.

20
Post #98
wmnnd

wmnnd

This is a neat idea, I like how explicit and intuitive to understand it is!

0
Post #99
vfsoraki

vfsoraki

I don’t like this part. I think making initialization explicit reads better:

iex> for let {sum, count} = {sum_var, count_var}, 
...>   i <- [1, 2, 3] do
...>     sum = sum + i
...>     count = count + 1
...>     {i * 2, {sum, count}}
...> end
{[2, 4, 6], {6, 3}}

Generally, adding this to comprehensions is nice. Kudos!

0
Post #100
sasajuric

sasajuric

Author of Elixir In Action

This is exactly how I feel about this topic. I like that for is getting map_reduce capabilities, but I dislike the let name. First, it introduces a completely new terminology for something we already have. But more importantly, this terminology misses the main purpose of such comprehension (to map and accumulate). So from that standpoint, for map (same as plain for for backwards compatibility), together with for reduce and for map_reduce seem perfect to me.

I don’t share the interpretation that for means map. To me a construct like for x <- 1..3, x != 2, ... always reads as “for every x in 1..3, x not equals 2, … do something”. In other words, IMO the word for communicates about the traversal, not about the outcome of such traversal.

For completeness, I also privately proposed three separate special forms: map (current for), reduce, and map_reduce. However, José pointed out that this would break a lot of the existing code. Perhaps if Elixir was designed from the ground up that would make sense, but as it is, for map, for reduce, and for map_reduce are the options I like the best, both as the user of the language, but also as a mentor and a book author who will have to explain this stuff to other people :slight_smile:

36
Post #101