Do we want `forEach`?

If we’re having a religious argument, I’m strongly on the side of .forEach. One person’s “this is ugly code” is another person’s “this is much more readable code.” I like the constraints of .forEach, and knowing that it’s going to process everything and there’s not extra BS happening (compared to a general-use for() loop).

If I glance at a .forEach there’s a bunch of stuff I just instantly know about the code, which I think is super-valuable.

7 Likes

Except this isn’t really true. return in a forEach is a for-loop’s continue, and functions that throw exit early without processing all elements, like a break.

9 Likes

Fair. But I still think as a capper for a chain of transforms, .forEach can’t be beat. I personally would rather have the UNIX pipe metaphor over a BASIC ‘assign a variable and use it later’ metaphor.

8 Likes

Serious question: how often would we expect the use of .forEach to be actually just wrapped in a task?

e.g.:

let doingStuff = Task {
  try await someLongDefinition.map { ... }.filter { ... }.forEach(doStuff)
}
2 Likes

I use it a lot. Please don’t get rid of it.

Ex: aBunchaViewsToHide.forEach { $0.isHiddden = true }

The discussion is not about whether to get rid of it, it's about whether to add an AsyncAlgorithms version of it.

That said, I don't understand what you're gaining from that. Isn't that exactly the same as a for loop, except with a less clear variable name ($0)?

It's more than a less clearly named for in ... loop. It's useful and flowy at the end of a long chain of higher-order functions such as filter, map, ..., as Wil described. I believe it's actually more clear than assigning the result to a throwaway variable if all you're doing is iterating over the result.

If anything, I'd be in favor of giving .forEach more power by having it return its input sequence as a discardable result, so .forEach could be used in the middle of such a transformation chain as well.

1 Like

I meant the specific case in the post I was replying to… but I see Discourse hid the reply relationship because they were adjacent. Unhelpful of it in this case, ah well.

2 Likes

I'm not sure what you're asking here. Why is wrapping in a Task relevant here? I would expect this forEach to be equally applicable whether wrapped in a Task or not, but I would think most use would eventually (in the medium term) be in naturally async methods.

Personally, I think we should have forEach. Not just for parity with Sequence, but because it lends itself well to one of the fundamental ways of using Swift, chaining. You can make the same arguments used against forEach against pretty much the entire rest of Swift's collection APIs. Doesn't make them bad, just means users need to know you use them differently than raw loops. So far that doesn't seem to have been an issue. That a tool isn't useful in 100% of cases isn't a reason not to have the tool.

In fact, I'd go farther. I'd say forEach's signature should change to forEach(...) -> Self, so that later operations can be chained. This is a nice increase in utility for a very small change.

5 Likes

The reasoning was more so to understand how folks intend or see its use. It was a pattern that I have been seeing emerge when using some of these things in app contexts and I was wondering if some of the use cases folks were seeing were similar to that of .sink (but perhaps a better name).

How would that even work? AsyncSequence is not double-pass capable. So unless it is an escaping "diagnostic window for side effects" (which I have much stronger objections that Ben's objection to .forEach on... mainly because it makes thread safety and self consistency virtually impossible because it breaks the monad's encapsulation), im not sure I follow on how that could be achieved.

forEach was useful with UIKit due to the abundance of classes, but it will continue on the trip towards complete worthlessness unless two things are fixed:

  1. Cannot reference 'mutating' method as function value.

I.e. it's only good when you already have an existing named closure to use with it.

[1, 2, 3, 4].forEach(&myStruct.add) ?

2 Likes

Ah, I see what you're getting at. Yeah, I've been using Task's to start stream observer callbacks, but I've just been using raw loops since there is no forEach. Doesn't seem like there's any other option yet. forEach makes that slightly nicer for more complex chains.

You're right, I didn't think that through for the async case. In that case it seems easy to just return a custom type like all of the other async operators, one that just passes elements through.

1 Like

Yes, it would be nice to lift those language limitations, but those are hardly forEach's fault.

1 Like

This is why I would like to create a way for return, break, and continue to work in closures so that functions like forEach can behave and look more like for loops.

I've met this feature in Ruby, and I loved it (and I guess it's initially a Smalltalk thing). In Ruby, we distinguish procs from lambdas. Procs affect control flow one-level up. Lambdas can not.

Ruby procs & lambdas
# prints
# lambda 1: foo
# lambda 2: foo
# proc 1: bar
# f: bar
def f
  l1 = lambda do 'foo' end
  puts "lambda 1: #{l1[]}"

  l2 = lambda do return 'foo' end
  puts "lambda 2: #{l2[]}"

  p1 = proc do 'bar' end
  puts "proc 1: #{p1[]}"
  
  p2 = proc do return 'bar' end
  puts "proc 2: #{p2[]}"
  
  puts "end"
end
puts "f: #{f()}"

There are other funny Ruby features, such as breaking with a value:

def f
  [1, 2, 3].each do |x|
    break x * 2 if x == 2
  end
end
f # 4

Now of course, changing Swift's forEach behavior would be a breaking change:

func f() {
  [1, 2, 3].forEach { x in
    guard x != 2 else {
      // Returning from f would be a rubyism, and a breaking change
      return
    }
    print(x)
  }
}
2 Likes

Why else would you want this features? A popular request is for SwiftUI’s ForEach, but that could be addressed with a lazy for-loop build method in result builders.

forEach helps to avoid declaring a variable where it's not needed. Does not have to be a long chain, can be take this, filter, do that:

let items = [...]
let filteredItems = items.map { ... }.filter { ... }

for item in filteredItems {
 // ...
}

vs.

let items = [...]

items.map { ... }
  .filter { ... }
  .forEach { item in
    // ...
  }

In fairness, you can still do it without an extra variable:

let items = [...]

for item in (items.map { ... }.filter { ... }) {
 // ...
}

although it is neither nice nor scaleable:


items
    .map { ... }
    .filter { ... }
    .sorted { ... }
    .compactMap { ... }
    .something { ... }
    .somethingElse { ... }
    .and { ... }
    .so { ... }
    .on { ... }
    .and { ... }
    .so { ... }
    .forth { ... }
    .forEach { .... }

vs

for item in (items.map { ... }.filter { ... }.sorted { ... }
    .compactMap { ... }.something { ... }.somethingElse { ... }
    .and { ... }.so { ... }.on { ... }.and { ... }
    .so { ... }.forth { ... }) {
 // ...
}

perhaps formatted like so to make it ok-ish?

for item in (
    items
        .map { ... }
        .filter { ... }
        .sorted { ... }
        .compactMap { ... }
        .something { ... }
        .somethingElse { ... }
        .and { ... }
        .so { ... }
        .on { ... }
        .and { ... }
        .so { ... }
        .forth { ... }
) { ... }

1 Like

This formatting is not acceptable to me within the for in body. I also tend to avoid the same with guard statements, for example:

Ugly:

guard let item = items.first(where: { item 
  // ...
}) else {
 // ...
 return
}

Good:

let item = items.first {
  // ...
}

guard let item = item else {
  // ...
  return
}

I use both extensively in my code, depending on context, to express emphasis and focus the readers attention: forEach to emphasize the collection as a whole, for in ... to focus attention on what is happening to each element.

1 Like