Putting a type constraint in the LHS of a foreach header (where the enumeration variable is defined) doesn't work. In fact, the parser gets very confused. See below:
> foreach ([int]$i in 1..10) { $i }
1
2
3
4
5
6
7
8
9
10
At line:1 char:10
+ foreach ([int]$i in 1..10) { $i }
+ ~
Missing variable name after foreach.
The correct form is: foreach ($a in $b) {...}
At line:1 char:18
+ foreach ([int]$i in 1..10) { $i }
+ ~~
Unexpected token 'in' in expression or statement.
At line:1 char:26
+ foreach ([int]$i in 1..10) { $i }
+ ~
Unexpected token ')' in expression or statement.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : MissingVariableNameAfterForeach
> $PSVersionTable
Name Value
---- -----
PSVersion 6.1.0-preview.1
PSEdition Core
GitCommitId v6.1.0-preview.1-61-gca5aacca16a18394427a411d1ca21e88a7d8a37b
OS Microsoft Windows 10.0.16299
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
@rjmholt I have no objections to adding this but I'm curious why you want to? Completeness perhaps? We've thought about it before but it never seemed particularly compelling, especially since the loop doesn't introduce a new scope the way it does in C#. Also note that you _can_ add a type-constraint to the variable by simply initializing it before the loop as in:
[int] $i = 0
foreach ($i in 1, "two", 3) { "i is $i" }
This code will error when it hits "two".
My only real motivation is the fact that the error messages the parser spits out when you try this are pretty incoherent. So ideally we fix that so the parser has a better concept of what the user's intent was and goes to the correct error state.
But if we're putting the work in there, it seems like we may as well support the actual feature. The $i part of foreach ($i in 1,2,3) {} constitutes a variable definition, so adding this we can better claim that variable definition can by constrained with a type in PowerShell. It's not obvious what we buy in the case of [int], but in a more complex case, perhaps with a heterogeneous list, the type constraint could aid static analysis and completions, or provide a simple mechanism for coercive assignment. There are other ways to achieve it, but this is maybe pithier.
To be clear, I'd like to fix the error messages. The full feature isn't necessarily something I want, but seems worth discussing.
The error message actually seems clear to me. But I'm special :-)
constitutes a variable definition
Well - we don't actually have variable definitions in PowerShell, except for class variables. But the variable in a foreach loop does constitute an assignment and we allow you to attach attributes to a variable during an assignment so it's reasonable to support this
foreach ([int] [validaterange(0, 1000)] $i in 1..100) { $i }
But of course the attributes on a variable are manipulable objects not a fixed definition as in:
PS[1] (131) > [int] [validaterange(0,1000)] $x = 123
PS[1] (132) > (gv x).Attributes
MinRange MaxRange TypeId
-------- -------- ------
0 1000 System.Management.Automation.ValidateRangeAttribute
System.Management.Automation.ArgumentTypeConverterAttribute
PS[1] (133) > $vra = (gv x).Attributes[0]
PS[1] (134) > (gv x).Attributes.Remove($vra)
True
PS[1] (135) > (gv x).Attributes
TransformNullOptionalParameters TypeId
------------------------------- ------
True System.Management.Automation.ArgumentTypeConverterAttribute
PS[1] (136) > $x = 2000
PS[1] (137) >
Ah that's really cool! Thanks!
One question I have here is what should happen if a validation attribute on the variable fails? Does the loop terminate or does it keep going and just spit out errors.
I have a working prototype here, which I've implemented with fairly minimal changes.
A couple of examples:
> foreach ([int]$x in 1..10) { $x }
1
2
3
4
5
6
7
8
9
10
> foreach ([int][char]$x in 'a'..'z') { $x }
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
And the current semantics terminate the loop when an error occurs in the validation (I think):
> foreach ([int]$x in '1','2','three','4') { $x }
1
2
Cannot convert value "three" to type "System.Int32". Error: "Input string was not in a correct format."
At line:1 char:10
+ foreach ([int]$x in '1','2','three','4') { $x }
+ ~~~~~~~
+ CategoryInfo : MetadataError: (:) [], ArgumentTransformationMetadataException
+ FullyQualifiedErrorId : RuntimeException
> foreach ([validaterange(1,5)][int]$x in 1..10) { $x }
1
2
3
4
5
The variable cannot be validated because the value 6 is not a valid value for the x variable.
At line:1 char:10
+ foreach ([validaterange(1,5)][int]$x in 1..10) { $x }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : MetadataError: (:) [], ValidationMetadataException
+ FullyQualifiedErrorId : ValidateSetFailure
But wondering how others feel about this. Or does this just prove that attributes on enumeration variables are silly?
A validation failure should be an exception (so e.g. it could be caught), so termination of the loop makes sense.
@rjmholt love the look of this -- it's always been a bit of a weird quirk that foreach-defined variables are kind of type-less.
PR when? 馃槃
I have a branch for this, but I was hitting a very odd error in PowerShell's variable analysis code. At that point other priorities caught up with me. I'll see if I can look into it again at some point.
Most helpful comment
A validation failure should be an exception (so e.g. it could be caught), so termination of the loop makes sense.