Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add method for foreach that takes no function #45152

Open
jlapeyre opened this issue May 2, 2022 · 5 comments
Open

add method for foreach that takes no function #45152

jlapeyre opened this issue May 2, 2022 · 5 comments
Labels
feature Indicates new feature / enhancement requests

Comments

@jlapeyre
Copy link
Contributor

jlapeyre commented May 2, 2022

People (in general) are familiar and comfortable with comprehensions. For example [f(k, v) for (k, v) in dict].
But, if I only want to call f(k, v) for side effects, using a comprehension is slow/wasteful. It would be nice if there were a way to write something as close as possible to a comprehension, but that does not store results.

I can do this.

for (k, v) in dict
    f(k, v)
end

Or this foreach(((k,v),) -> f(k, v), dict). The latter is OK, but I avoid it; for example when sharing code with people new to Julia. (although using a trailing comma to mean "create a tuple" is also known in Python.)

So far, the best solution I see is this

foreach(f(k, v) for (k, v) in dict)

This would work with the following method.

Base.foreach(g::Base.Generator) = foreach(identity, g)

Arguments in favor:

  • It is clear immediately to any Julia user and many newcomers what foreach(f(k, v) for (k, v) in dict) does. (Maybe a few would expect this to be equivalent to a comprehension)
  • It adds no verbs or nouns to Base
  • It is a natural, unsurprising, extension to foreach. It fits very well with foreach's mission.
  • It is very easy to read and understand the new code in Base. It adds very little complexity.
  • There are no other contenders for what foreach(::Generator) should mean.

Some people don't like foreach. There are reasonable arguments against it. So if your goal is to move towards deprecation and eventually removing foreach (maybe in v2) then extending its use would not be attractive.

For me, it's not foreach that is so attractive iteself. I'm just looking for a way to write it that is as close to comprehension/generator syntax as possible. This means how can you most simply iterate over a generator while discarding values ?

@jlapeyre
Copy link
Contributor Author

jlapeyre commented May 2, 2022

Just found this, related, comment: #19198 (comment)

@jlapeyre
Copy link
Contributor Author

jlapeyre commented May 2, 2022

Another idea, that I like

julia> (g::Base.Generator)(f=identity) = foreach(f, g);

julia> (println(x) for x in 1:3)()
1
2
3

But, that's a bit weird. There is also the question of whether/how to extend this, or the original idea for foreach(::Generator) to other iterators.

@JeffBezanson
Copy link
Member

foreach(((k,v),) -> f(k, v), dict)

The most stylish way to do this is foreach(Base.splat(f), dict). Maybe we should finally export splat.

I do think it would be neat if foreach(itr) just ran through the iterator without doing anything else. Unfortunately it conflicts with the desire to have higher-order functions like map(f) curry on the first argument (i.e. we would have foreach(f) = itr->foreach(f, itr)).
Maybe we can think of another name for this operation. iterateall?

@jlapeyre
Copy link
Contributor Author

jlapeyre commented May 2, 2022

I think foreach is already ruined for currying. It has this method: foreach(f) = (f(); nothing). This method is precisely for the case you want. So doing something else for things that aren't callable at least reclaims some space. There appears to be no foreach call with a single argument in base and stdlib. On the other hand, if you want foreach(f) to curry in v2, then this is perfect, you won't break any code.

I kinda like discard, after this comment: #19198 (comment).

discard(v[i] = w[i] + 1 for i in eachindex(w))

If you are thinking about the argument as an iterable, then it's pretty clear what this does. But, if you are a casual user, you might think, "it does that thing I want just as I wrote, but they told me I have to write 'discard', too. What's that about? Does it throw away w?" I mean, in this case the line is clearly intended for its side effect, so the computer must understand what I want. Another possibility is consume, but it has the same problem. (or how about eat, it's suitably short)

iterateall might strike a good balance between signaling intent to the programmer/developer and the casual user.

Another possibility is run(v[i] = w[i] + 1 for i in eachindex(w)). You could say that this is too far from the meaning of run(::Cmd). But, BenchmarkTools also uses run for something else, and alot of people have it imported all the time.

@vtjnash
Copy link
Member

vtjnash commented May 3, 2022

We export Splat now

@nsajko nsajko added the feature Indicates new feature / enhancement requests label Jul 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Indicates new feature / enhancement requests
Projects
None yet
Development

No branches or pull requests

4 participants