To add a little more fuel to the flame that is the two existing proposals for multi-line switch expressions, I'd like to offer a third option, to see if it's preferred, or stirs up an even bigger flame war.
Issue description
You want to create a value, but need control flow logic in order to do so. i.e.
if condition {
let value = 3
}
else {
let value = 4
}
// But we can't use `value` here because
// it's scoped to inside the if blocks
A more common case might be with do-catch
when initializing the value:
do {
let value = try initValue()
}
catch {
let value = fallback
}
Existing solutions/workarounds
Ternary
let value = condition ? 3 : 4
However, this only works for a single-line option. If you want to do anything extra like
if condition {
let value = 3
}
else {
doSomethingExtra()
let value = 4
}
It no longer translates to a ternary. Also, ternary only works for if
, and not for do
or switch
.
Just use let
earlier
Surprisingly enough, this works:
let value
if condition {
value = 3
}
else {
value = 4
}
print(value)
Even though let
is used, Swift simply makes sure that value
is only assigned once.
IIFE
Abusing closures, the following is possible:
let value = {() in
if condition {
return 3
}
return 4
}()
Bonus points:
- We can skip the
else
- It works with
for
too:
let value = {() in
for element in sequence {
if test(element) {
return map(element)
}
}
return defaultValue
}()
But, the use of closures breaks other control flow elements:
for element in sequence {
let value = {() in
if test1(element) {
return 1
}
if test2(element) {
continue // Oops! We're not actually "inside" the loop
}
return 2
}()
print(value)
}
A bigger problem is that return
changes its meaning, so you can "return" the value to the assignment, but you can't break out of the outer function inside the IIFE. At least, not without abusing exceptions as well.
Proposed solution
Allowing labelled breaks to be used in an expression and have a value:
let value = mylabel: if condition {
break mylabel(3)
}
else {
break mylabel(4)
}
Can also remove the else
by using do
, as well as adding additional statements:
let value = mylabel: do {
if condition {
break mylabel(3)
}
doSomethingExtra()
break mylabel(4)
}
And, like IIFE, it can apply to for
as well:
let value = mylabel: do {
for element in sequence {
if test(element) {
break mylabel(map(element))
}
}
break mylabel(defaultValue)
}
And out control flow can still be used using labels:
outer: for element in sequence {
let value = inner: do {
if test1(element) {
break inner(1)
}
if test2(element) {
continue outer
}
break inner(2)
}
print(value)
}
Note that the label is a must:
let value = if condition {
// Am I trying to return 3 from the `if`,
// Or break out of a label called `3:`?
// This will always be interpreted as the latter,
// and cause an error if no such label exists
break 3
}
else {
break 4
}
While the label's return type can be automatically be determined from the break
's value type, you might prefer to define it explicitly:
let value = mylabel(Int): if condition {
break mylabel(3)
}
else {
break mylabel(4)
}
Other concerns
Unlike the existing proposals, this proposal:
- Does not introduce a new keyword
- Does not break source compatibility in any way (
break label(value)
would currently be a syntax error)
One caveat is that the use of labels inside an expression can conflict with ternaries due to the use of :
. To remove ambiguity, ternary takes precedence. If you want to use label with a value inside a ternary, simply wrap it with ()
, as so:
let value = condition1 ? (mylabel: if condition2 {
break mylabel(1)
}
else {
break mylabel(2)
}) : 3
Without the ()
, mylabel:
above would be interpreted as taking the value of a variable called mylabel
, and then moving to the else
part of the ternary. At which point, the final : 3
would cause a syntax error, and break mylabel(1)
would cause an Error, no label named "mylabel" in scope
So, what does everyone think about this? Is it better than the existing proposals? Worse? Comment your thoughts below.