I’d like to explore something to see if it has any resonance with the larger community here.
In summary, I’d like to be able to write statements similar to :
if foo == 1, let bar = foobar(), bar.value > 3 { /* code here that may use the value of bar*/ }
without the ability to add assignments into the logic, I find myself with the alternative of :
if foo == 1 { let bar = foobar() if bar.value > 3 { /some code here that uses bar*/ } }
which is altogether less elegant. I realise of course that one may argue the following is ok:
if foo == 1, foobar().value > 3 { let bar = foobar().value /*more code here*/ }
However that means we have to repeat the call to foobar(), and if that assignment was somewhat complex, another reason to disfavour this approach is the increased risk that the statements will get out of sync.
Could we consider allowing assignments within conditional logic and implicitly assert their value as true for the purposes of evaluating the result?
Thanks for reading.
Please accept my apologies in advance if a) this is not the right forum for such questions, or b) this idea has been previously raised and rejected. (I did look!)
The syntax Jonathan is using here is the pattern matching syntax. A more typical usage of it would be something like if case .foo(let bar) = baz to unwrap an enum. However, there are a number of non-enum pattern matching expressions. For example, if case let foo? = foo is the long-form version of if let foo. And it falls naturally out of being able to unconditionally bind an enum’s associated value (the let bar above) that you can do if case let foo = foo to create a condition that is always true.
Recommendation: always check this site to make sure what you want isn't already hidden in the bowels of the language before bothering to take up your time writing up a post.
I’ll be sure to check that out in future. In this case though, I think it’s been useful to highlight that this is possible now, as I get the sense I’m not alone in being unaware of this somewhat surprising use of case.
Some case expression, like in this context, make very little semantic sense. Swift has been due to revisiting these a long time, more notably a new syntax to take the place of the Yoda-style simple case comparisons.
In this context, however, the path to more straightforward code seems easy: relax rules for let expressions in conditions, which now require assignment to an optional, so that when not an optional it behaves just like this case let.
However it may not be that simple. What am I overlooking?
The existing rules ban that because it’s a dead branch, a nil check that always succeeds, which often indicates a programming mistake (or a moment mid-refactor that you want to attend to). case let x = y, however, is always equivalent to an assignment—it never transforms y in any way, and if you were expecting it to unwrap, well, you’ll get an error soon enough. So it’s much less likely to be a mistake.
That said, I have no idea whether this was intentionally allowed. I suspect not.
A possible relaxation of the existing rules could allow plain let x = y for non-Optional y when there is another condition afterwards, and require putting it inside the block otherwise. That could certainly still let some mistakes in, but there’ll be at least one check that keeps the else branch from being dead. I’m not sure if I overall think it’s worth it, but if a change is to be made, I think that’s the one I would make. (And the diagnostic for the final clause would still just be a warning, not an error; it doesn’t need to block the build.)
So to accomplish an assignment in a condition statement, e.g. let x = y there are actually two ways ( case let x = y and let x = Optional(y) ) both of which add semantic noise / irritation, with one of them relying on an arcane and unintuitive use of case.
I must admit to having zero current understanding of what work would be necessary to relax the existing rules that ban the logical assignment, but I’d be happy to support, in whatever way I can, the effort to do it. This type of assignment is something I’d like to do relatively frequently. As I don’t dare to think I am in any way unique, it must be something others would find “nice” also.?
// (C)
if foo == 1,
// I guess the value of `foo` is obvious here, but what if it's not equality
case _ = print("foo = \(foo)"),
let bar = foobar(),
case _ = print("bar.value = \(bar.value)"),
bar.value > 3
{
// ...
} else {
// ...
}
I don't recommend this exactly, but I'd be lying if I said I haven't used it a couple times in a pinch where I couldn't fire up a real debugger.
if let and if var specifically mean unwrapping is involved. Your suggestion conflates let/var and if let/if var.
for loops have a precent for this, however:
for element in array
is shorthand for
for case let element in array
Like case, let doesn't do anything. So you can just get rid of it, because the assignment operator can't return a Bool or Optional.
if x = value
Extending that to all Void-returning expressions would allow for the elimination of the need for this as well:
case _ =
The same cannot be done with case var, however. We're stuck with that one.
Aside from redundancy, it is not a problem that these are the same thing, for defining constants for for loops:
for case var element in array
for var element in array
But again, the following line comes with the meaning that value is an Optional.
if var x = value
So we must stick with this:
if case var x = value
This is all with the caveat that if let/var performing unwrapping was not a good choice to begin with (without the additional ? that is required with case) , but that ship long ago sailed off a cliff.
// (A)
if foo == 1, let bar = foobar(), bar.value > 3 {
// some code here that uses bar
} else {
// We are not able to proceed.
// Time to clean up.
// This might take several lines of code.
// But we only have to write it once.
}
// (B)
if foo == 1 {
let bar = foobar()
if bar.value > 3 {
// some code here that uses bar
} else {
// We are not able to proceed.
// Time to clean up.
// This might take several lines of code.
// And now we have to write it twice.
}
} else {
// We are not able to proceed.
// Time to clean up.
// This might take several lines of code.
// And now we have to write it twice.
}
Yes, I aim to conflate. I'm in fact suggesting changing the rules so if let shouldn't exclusively mean unwrapping.
The kind of overlooked, less-than-simple wrinkle I expected is how, in an if like this:
if foo, let banana = giggidy(), banana.isGood {
...
}
one wouldn't be able to tell by looking that giggidy() returns an optional, causing expression to fail when its result is nil, or not. In other words, unwrapping and assignment would look exactly like just assignment.
Is this a problem for the user? One thing about if case let banana is that it looks different from if let banana, however part of my point is that case isn't a very useful way to distinguish those two situations because that keyword makes very little sense there. Is there a great need to have a different syntax for the "just assignment" situation, even if that syntax is wack?
I don't see right now how it would be a problem for the compiler for these to use the same syntax, would it ever be in a state of confusion about giggidy() returning an optional?
Also I imagine that degenerate cases like this would produce a warning for "just assignment" where it wouldn't for "unwrapping and assignment", just like it does using case let today:
if let i = notAnOptional { // warning: 'if' condition is always true
...
}
Thanks for the obscure syntax discussion, educational! Notably, I learned another example of case syntax that makes absolutely no sense, yippee.
The more obscure uses of case let come from the switch syntax that allows for purely renaming a value.
switch optionalInteger {
case let mightyNumber? where mightyNumber > 9_000: break
case let punyValue: break
}
}
if and guard don't have an implicit value to rename, so they require assignment.
if case let mightyNumber? = optionalInteger, mightyNumber > 9_000 { }
else if true, case let punyValue = optionalInteger { }
(You can't get rid of the if true without a warning, so use switch for this in practice.)
for case let element? in array
is useful.
for case let element in array
does not "make absolutely no sense", but it is useless and should probably generate a warning.
A note about the title of this thread ("conditional logic"): There's no requirement for there actually to be any condition. I.e. this compiles:
if case let x = 1, case _ = 2 { }
For reference, this is what my vote would have been, to eliminate the confusion and annoyance that the language now requires:
Don't require or allow case when using Optional sugar or Void-returning expressions.
Don't require assignments to _ for Void-returning expressions.
Require ? for unwrapping.
Allow for unconditional expressions after else, as a stylistic option for consistency.
if let mightyNumber? = optionalInteger, mightyNumber > 9_000 { }
else let punyValue = optionalInteger, print("Closing braces probably ought to be optional here as well because nothing else is going to happen with \(punyValue).")
An aside on case expressions, and how all manner of pattern matching mechanics from switch statements are allowed in other situations:
Consistency can be a good thing, leading to reinforcement of valid expectations, learners being able to construct metaphors and mental models, and, in the end, complex systems being comprehensible and usable.
However consistency can sometimes be like Windows 3.1 having so many OS features buried in a tiny Properties window opened by a right-click menu, ones that really should have been exposed more thoughtfully. Such consistencies can counter expectations and confound metaphors ("oh I didn't expect a properties window here like files have, maybe I don't understand what kinds of things have properties") and reduce usability (a big complex set of settings getting crammed into a little window)[1].
I say for case let element in array makes no sense because:
The keyword case seems to derive from cases of enums, used in the prototypical example of switch statements, then extended to apply to pattern matching within those statements. One can make the mental extension a la "in the case that the first element of the tuple is nil and the second is a int greater than 10" that has no connection to enums. Then extending the syntax for assignment to temp vars during matching, and pattern matching elsewhere like if statements, all great.
For assignment of element to members of array in a loop statement, there's nothing like pattern matching here. There's nowhere where "in the case that ..." applies in this construct, and certainly nothing regarding enums. This is the language design saying "we do assignment in case expressions, so for these other assignment expressions we'll just reuse that".
I claim this is an instance of detrimental consistency, just like Windows 3.1 saying "we put some detailed settings for files in a little properties window, for our complex set of networking parameters we'll just reuse that".
I think it was a bad choice to lean on "case" syntax in all the situations it's used in Swift. Some different syntax should have been invented for many of them, the poster child being the awkward backwards-speak of if case .yellow = banana, and its bizarre use of single "=" as if some assignment was happening.
Swift smartly allows a better syntax in for loops, allowing simply for element in array. I think this lesson should have been applied elsewhere.
If assignment of non-optionals within conditional expressions is something Swift should bring off its secret menu, ie. document and encourage, at the moment I don't see a downside for both if let foo = notAnOptional and if let foo = anOptional to share the same straightforward syntax, except doing so on principle. Can someone provide a convincing example?
I wouldn't be against migrating the canonical syntax for the latter to be if let foo? = anOptional, even though I suspect that's a big ask. I might lean towards preferring this use of "?" stay relegated to its corner case, since other places where foo? is used, the thing suffixed by the question mark starts out already an optional, or in case of types, it's being made an optional. Maybe I'm just not squinting at it the right way. I'd certainly be for allowing it and letting the developer decide what style they want in their code[2].
However, I'm also not convinced Swift even needs to bring this off the secret menu. Maybe its felt that assignment (or unconditional expressions), willy-nilly in conditions leads to poorly written code, and a slightly awkward syntax is good to discourage its used in the general population.
This is the example that came to mind when thinking about fiendish consistency, it's probably not the best ↩︎
like I thankfully can decide to avoid the shortcut if let thingie for unwrapping an optional since I find it bewildering ↩︎
I just want to remark that the full generality of pattern-matching syntax is usable in that position. For example:
enum Foo {
case a(Int)
case b(String)
}
let list: [Foo] = [.a(1), .b("2"), .a(3)]
for case .a(let x) in list {
print(x) // prints 1 and 3
}
I think this actually reads just fine, as also does switch syntax.
The odd one out, in my opinion, is if case let. Every single time I have ever looked at or reached for if case let, without exception, I have wanted the two sides to be in the other order.
I always want to write if <thing> matches <pattern>, and I never want to write if case let <pattern> = <thing>. This is both a matter of readability, and also of autocomplete.
The way it is now, writing the pattern before the thing it’s matching, there is no type context for autocomplete to go on. But if it were the other way around, then as soon as you type if <thing> matches then autocomplete can jump in and be helpful.