★ wanayoo — archive 1999 https://github.com/PowerShell/PowerShell-RFC/pull/222Nouvelle recherche | Portail wanayoo
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 an else clause to the foreach keyword #222

Open
wants to merge 4 commits into
base: master
from

Conversation

@kvprasoon
Copy link

kvprasoon commented Aug 22, 2019

No description provided.

kvprasoon added 3 commits Aug 22, 2019
@KirkMunro
Copy link
Contributor

KirkMunro commented Aug 22, 2019 •

@kvprasoon Can you please share sample code that demonstrates the problem you're trying to solve on PowerShell 6 or later? The closer you can provide to a real-world (non-contrived) example where this would be beneficial, the better.

Unless I'm mistaken, the bulk of this problem was resolved long ago (I forget in which version), and modern versions of PowerShell no longer process the internals of a foreach block when the collection that is being enumerated is $null or empty, and the only scenarios you describe in your RFC that are not covered are the "else" scenarios (i.e. run some logic if the collection is $null or empty, or if an item in the collection is null as per your alternative proposal).

@kvprasoon
Copy link
Author

kvprasoon commented Aug 24, 2019

Thanks for the comment @KirkMunro .

PowerShell no longer process the internals of a foreach block when the collection that is being enumerated is $null or empty

Yep and agreeing on that.

This RFC is actually a feature request to add the else statement capability to foreach loop.

Proposal 1 is from my experience as I've hit sometimes at work when an expected collection(supposed to get created) becomes empty and the subsequent foreach which is supposed to created/do something else using the content in the collection, which will not happen. So we normally have an if/else statement to check before reaching foreach . The alternate proposal something came to my mind after reading similar implementation doc in python

@KirkMunro
Copy link
Contributor

KirkMunro commented Aug 25, 2019

@kvprasoon I understand now. Ok, in that case, you might want to reword this RFC. For example, simply state the intent of the RFC (to add an else clause to the foreach keyword), then identify why this adds value (it provides a more elegant way to handle an unexpected empty collection being provided to foreach, without requiring an if/else clause to wrap the whole thing), mention that your source of influence for this is Python, and then show example syntax, identifying what is required without the else clause, and then show how it would work with the else clause.

Also, the way it is worded right now it sounds (to me at least) like this could introduce a breaking change, but that is not the case. That sentence could be removed or reworded.

@kvprasoon
Copy link
Author

kvprasoon commented Aug 26, 2019

Thanks, not a Native English speaker :-), I've refined it and added one more proposal.

@kvprasoon kvprasoon changed the title Foreach-else statement Add an else clause to the foreach keyword Aug 26, 2019
@KirkMunro
Copy link
Contributor

KirkMunro commented Aug 26, 2019

Now that I've re-read this, and now that I've read about python's for-else clause, I like their approach better and I would use it more. The proposals here don't seem to buy me much.

For example, instead of this:

foreach ($i in $collection) {
    ...
} else {
    # Handle empty collection
}

If I really wanted to handle an empty collection in a specific way, I would do this instead:

if (-not $collection) {
    # The collection was empty; write error and return
    Write-Error 'The collection was empty'
    return
}

# At this point I expect things to be in working order
foreach ($i in $collection) {
    ....
}

That avoids wrapping and unnecessary indentation, while focusing on handling the error scenarios up front when I can.

Also, regarding the alternate proposal, having an else clause that is outside of foreach but expected to fire once for each non-matching item feels like the wrong design for me, because the else script block does not appear to be a loop. In that specific scenario, where you're switching on types, better to just switch on the types inside the loop. If there's a syntax improvement to be had there, it is probably more in line with how C# compares types in switch statements, which it does quite nicely.

Lastly, I believe the current guidance (that will be shared in documentation soon that is being worked on) is for language improvement ideas to be shared in issues in the PowerShell repository, discussed there, and then RFCs can come from those discussions once things have been hashed out a bit.

@fowl2
Copy link

fowl2 commented Aug 28, 2019

foreach else improves on if (-not) foreach by only enumerating once without forcing buffering.

@KirkMunro
Copy link
Contributor

KirkMunro commented Aug 28, 2019

@fowl2 Can you explain the point that you're trying to make in more detail please?

@fowl2
Copy link

fowl2 commented Aug 28, 2019 •

Some examples:

Naive option:

if (-not (Get-Item -Some -Parameters | ? Filtering -eq 'stuff')) {
  "No items!" 
}

foreach ($item in (Get-Item -Some -Parameters | ? Filtering -eq 'stuff'))  {
  Do-SomethingWith $item
}

Cons:

  • I had to type Get-Item part twice - could easily make a mistake or forget to update one or the other
  • Get-Item might be expensive
  • Get-Item could return something different each time I call it

Buffering option:

$items = Get-Item -Some -Parameters | ? Filtering -eq 'stuff'
if (-not $items) { 
  "No Items" 
}
foreach ($item in $items)  { 
  Do-SomethingWith $item
}

Cons:

  • had to introduce a variable (extra typing)
  • buffers entire collection - whole thing must fit in memory
  • buffers entire collection - won't start processing untill all items are retrieved.

Flag variable option:

$hasItems = $false
foreach ($item in (Get-Item -Some -Parameters | ? Filtering -eq 'stuff'))  {
  $hasItems = $true;
  Do-SomethingWith $item
}
if (-not $hasItems) { 
  "No Items" 
}

Pros: works today
Cons: extra typing, extra variables to name

foreach else option:

foreach ($item in (Get-Item -Some -Parameters | ? Filtering -eq 'stuff'))  {
   Do-SomethingWith $item
} else {
  "No Items" 
}

Pros: shortest, none of the drawbacks of previous options
Cons: extra complexity in the language

@KirkMunro
Copy link
Contributor

KirkMunro commented Aug 28, 2019

Except the foreach keyword buffers anyway, right? So you wouldn't be gaining much.

@vexx32
Copy link
Contributor

vexx32 commented Aug 28, 2019

Yeah, if you're using foreach you have to have the whole collection in memory regardless. That's why ForEach-Object can be so handy to work with.

But I still think there's value in the foreach/else syntax regardless. 🙂

@KirkMunro
Copy link
Contributor

KirkMunro commented Aug 28, 2019 •

But I still think there's value in the foreach/else syntax regardless. 🙂

I agree there's value in foreach/else. I just wonder if it's better to handle unexpected scenarios up front by checking for an empty collection first or if it's better to handle then inline with the proposed foreach/else syntactic sugar. Thinking more about this now, that depends on the scenario, in which case having the option would make sense.

If this does move forward, it would make sense to add for/else and while/else as well, to the point that I wonder if this RFC should be expanded to be about else clauses for all three loops (foreach, for, and while).

@vexx32
Copy link
Contributor

vexx32 commented Aug 28, 2019

Yep, absolutely, I'd say if we do this we should have this for all our loops. The only exception is probably our do/while and do/until loops, which don't have a check at the start of the loop.

@SeeminglyScience
Copy link

SeeminglyScience commented Aug 28, 2019 •

Yeah, if you're using foreach you have to have the whole collection in memory regardless. That's why ForEach-Object can be so handy to work with.

Depends what the source is. Most IEnumerable objects can be used there, but some actually generate their value on each MoveNext call. e.g.

using namespace System.Linq

$enumerable = [Enumerable]::Select(
    [int[]](0..2),
    [Func[int, int]]{
        param($x) end {
            Write-Host "Processing $x"
            Start-Sleep 1
            return $x * 2
        }
    })

# This is actually always false, since PS can't check it's length effeciently.
if (-not $enumerable) {
    return
}

# The select delegate is invoked every iteration.
foreach ($item in $enumerable) {}

This raises another question. Since PS can't easily tell if the enumerable is empty, how does else work here? Does it keep a local bool and set it after MoveNext returns true at least once? or does it use current truthy logic?

@kvprasoon

This comment was marked as off-topic.

@vexx32
Copy link
Contributor

vexx32 commented Aug 28, 2019

That's a good question there, Patrick... I think in the case of <loop>{} else{} it might be a better idea to opt for your first alternative, actually checking to see if the collection's enumerator returns anything.

This would allow folks to work better with less coherent collections and would overall be a benefit, I think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

5 participants
You can’t perform that action at this time.