Could Swift's patterns be more uniform?

I'm starting this thread since there seems to be a lot to discuss related to Swift's patterns.

The subject has come up in at least three recent threads:
Allow optional unwrapping in where clause
Unwrapping and value binding patterns in the condition of ternary expressions
SE-0231 — Optional iteration

It would be interesting to see if Swift's patterns (in general, not just optional patterns) could be made more uniform, and thus easier to use/understand/remember/learn/teach, while at the same time, if we're lucky, solve problems like the ones in the above threads as a by-product.

If not, we can at least learn something by understanding the reasons for why that is not the case.


As an initial example:

let optInt: Int? = 123
let optInts: [Int?] = [123, 456, 789]

// Note that
// `case let .some(v)` works for both the if statement and the for-in loop:
if  case let .some(v) =  optInt  { print(v) } // prints 123
for case let .some(v) in optInts { print(v) } // prints 123 456 789

// and
// `case let v?`, a terser form, also works for both:
if  case let v? =  optInt  { print(v) } // prints 123
for case let v? in optInts { print(v) } // prints 123 456 789

// but
// `let v`, the tersest form, only works for the if statement, WHY?
if  let v =  optInt  { print(v) } // prints 123
for let v in optInts { print(v) } // ERROR: 'let' pattern cannot appear nested
                                  //        in an already immutable context
// (I don't understand that error message.)

And related to for-in loops and SE-0231, could the right side of the in do some pattern matching too, in some way that is consistent with other uses of patterns in the language?

9 Likes

As a convention and for definitiveness, foo in if foo = optInt is treated as an unresolved identifier, because if can take boolean expressions as well. One might imagine we could have had the compiler figure out whether if foo = optInt is a boolean expression or an optional binding pattern, so that we could omit let here as well. But doing so implies both unnecessary implementation complications and counterproductive situations from the user's perspective: the compiler often has no way to tell whether foo = optInt is a misspelled equality operator or an intended optional binding pattern. Naturally, the less often the compiler has to guess, the better.
In a for-in statement, we are sure that an unresolved identifier following for is the element, so we can afford to make let the default mutability semantic and omit it. But I do agree that enforcing the latter isn't reasonable relative to the model. This should had been made an optional convenience.

I'm not sure I understood everything, but I do see that the current situation, ie these two:

for     v in optInts { print("still wrapped:              ", v ?? ".none") }
for var v in optInts { print("still wrapped (and mutable):", v ?? ".none") }

would not make sense with:

for let v in optInts { print("unwrapped:", v) }

I think aligning it with if, like the following, would make more sense though, for convenience and simplicity:

for     v in optInts { print("still wrapped:          ", v ?? ".none") }
for var v in optInts { print("unwrapped (and mutable):", v, "just like if var v = …") }
for let v in optInts { print("unwrapped:              ", v, "just like if let v = …") }

or even better, require an explicit ? to unwrap, because doing it implicitly is potentially confusing (where did the .some(…) go?) and complicates the rules, then we would have:

for     v? in optInts { print("uwrapped:               ", v) }
for var v? in optInts { print("unwrapped (and mutable):", v) }
for let v? in optInts { print("unwrapped:              ", v) }

and also require it for if statements.


EDIT: So can this inconsistency be fixed, or why exactly can't it?

2 Likes

I don't think we can consistently expand optional binding syntax to for-in loops. There's the element identifier after all, which would conflict with an optional binding pattern. Optional patterns are the best alternative to optional binding when it isn't available, i.e. for-in, switch cases:

for case let unwrapped? in optInts { ... }

switch optInt {
case let x? where condition: ...
...
}

Sorry for being slow, could you show an example of how the element identifier would conflict with an optional binding pattern?

The simplest case is for x in sequence. It makes sense to also be able to write for let x in sequence as an equivalent, but if I understand correctly, you suggest that to be an optional binding pattern instead. If so, how do we tell it apart from the existing for var x in sequence syntax?

Note that it can also be argued that it is the current for let/var x in sequence that is inconsistent in that it looks like an optional binding pattern without being one, ie x looks like it's being optionally bound to each (optional) element in the sequence, but it isn't.

Afaics, the deeper cause of both inconsistencies is the current special case of allowing pattern matching in a condition to implicitly unwrap optionals, which while being slightly convenient (it's a ? less to type) results in this kind of confusion.

So the solution would be to remove the special case, and require explicit unwrapping everywhere, as in eg:
if let e? = optInt { … }
and we'd get:

for     v in optInts { /* v is still wrapped */ }
for let v in optInts { /* v is still wrapped */ }
for var v in optInts { /* v is still wrapped (and mutable) */ }

and

for     v? in optInts { /* v is unwrapped */ }
for let v? in optInts { /* v is unwrapped */ }
for var v? in optInts { /* v is unwrapped (and mutable) */ }

It would be an error to

for let/var v? in seqOfNonOptValues { … } // ERROR: Initializer for conditional binding must have Optional type, not 'Int'

just like it would be to

ìf let/var v? in nonOptValue { … } // ERROR: Initializer for conditional binding must have Optional type, not 'Int'

This has been brought up on this forum multiple times in the past; I can't recall the exact arguments but a search should easily surface information about why it has been rejected.

2 Likes

Thanks, it would be interesting to see why. I spent a couple of minutes searching the forum without finding anything though.

Here’s what a quick inbox search of the old SE mailing list turned up:

Edit:

Topic title is “Obsoleting `if let`”, and the first post references a previous topic called “The bind thread”.

1 Like

Huh, so we have these inconsistencies and complex rules that are hard to describe/learn/remember/reason about etc because people are unwrapping optionals too often to be comfortable typing a ? each time ...

I think that is a bit sad and worrying.

Will read the whole thread some time.

2 Likes

It is a little unfortunate that switching for var x in xs escaped notice when the ability to make things var/let was removed elsewhere (e.g. method parameters), because I think similar arguments apply here for why it is confusing (e.g. people might think that by mutating x its value will change in xs, in the same way that people were confused about mutating a method parameter in the method body and the change not being reflected at the call site). I believe there's some plan to eventually allow this kind of mutation with inout variables or borrowing or similar, though.

1 Like

People will always be confused while learning something new.

What's important is whether the language …

  1. … allows for only a few misconceptions and effective explanations.

  2. … leads to a never ending loop of misconceptions and inefficient explanations.

That is: Do the explanations …

  1. … work together to unveil a few powerful and highly composable concepts by which users can quickly understand the language as a whole (despite/thanks to being really stumped a few times)?

  2. … only make superficial sense in isolation, and cause further confusion about other parts of the language (despite/thanks to being really convenient and pragmatic in a few contexts)?


2 is easy, 1 is hard, so 2 is what happens without an active will to pursue 1, and I wish SE could be more about 1 than 2.

I'm not sure if you are replying to something I specifically said or if you are just talking generally. I was only saying that the ability to mark the loop variable as var in a for-in loop should probably have been removed at the same time as the ability to mark a method parameter as var. This would have made it more palatable to allow let in for-in loops like you mention in your first post. It's presumably too late to remove now, though.

Oh, it was only a general reflection I made after your post and @Nevin's post. If SE had been more about 1 than 2, then this and many other situations could have been prevented.

I guess I'm just letting out a bit of the frustration I feel when trying to to deal with the fact that spotty convenience (and the complexity it leads to) is a clear winner over trying to accomplish a truly great conceptual design (and the simplicity and power that can be gained from that).

1 Like

Mutating x in xs will come with the new ownership system.

for inout employee in company.employees {
  employee.respected = true
}

Edit: Oh silly me, should have read you post until the end before responding. :sweat_smile:

Is that just a sketch or will inout be written before employee, as in:

func f(inout employee: Employee) { /* … */ } // ERROR: 'inout' before a parameter name is not allowed, place it before the parameter type instead

?

The feature's resemblance with (what's intended in) the above and the resulting error message would probably lead most people to expect:

for employee: inout in company.employees { … }

Then comes the question: What is the lhs in a for lhs in rhs { … } statement?
Is it like a pattern matching expression where you can write eg case let .some(e) or is it like a parameter declaration or is it a unique mix of a bit of both? The grammar currently says "caseopt pattern":

GRAMMAR OF A FOR-IN STATEMENT

for-in-statement → for caseopt pattern in expression where-clauseopt code-block

And why is the where clause together with the rhs (to which it doesn't apply) rather than with the lhs which it does apply to? And isn't it more like a guard than a where? (cc @Erica_Sadun, @beccadax)

Help us Erick Meijer and Conal Elliott!

That was a copy of the code sample from the ownership manifesto. I think @John_McCall can provide a correct answer to that question.

1 Like

I’m sure we’ll take about that when it’s proposed.

3 Likes