Better not to overload the meaning of the return
.
How about yield
instead?
func foo(_ bool: Bool) -> Int {
let value = if bool {
42
} else {
yield 21
}
return value + 1
}
Better not to overload the meaning of the return
.
How about yield
instead?
func foo(_ bool: Bool) -> Int {
let value = if bool {
42
} else {
yield 21
}
return value + 1
}
To clarify, are you suggesting that if
expressions with multiple-statement branches should have more limited type inference than single-expression branches? This seems to go against your earlier arguments that single-expressions should easily be evolvable into multiple statements:
Changing type inference when you evolve an if
expression with single-expression branches into multiple-statement branches seems like that would be another source of frustration. We certainly found that to be the case for single-expression closures versus multi-statement closures - having to annotate the return type of multi-statement closures was a huge pain point until SE-0326 was implemented.
Could you please elaborate on why you believe changing type inference behavior between single-expression and multiple-statement branches would not lead to the same sorts of frustration/confusion? Or please correct me if I've misunderstood your suggestion!
Will if
and switch
expressions be allowed for property initializers?
struct T {
var x: Int = Bool.random() ? 1 : 0
var y: Int = if Bool.random() { 1 } else { 0 }
var z: Int = switch Bool.random() { case true: 1 case false: 0 }
}
Swift 5.7.1 allows property x
(with the ternary expression).
Yes — though not advocating so much as wondering out loud. I’m casting about for some policy that preserves parity with ternaries without creating a type inference performance explosion.
Yes, that certainly is a flaw (likely a fatal flaw) in my suggestion!
Your mention of SE-0326 might be useful guidance. @jrose mentioned a hypothetical choose
function upstream; let’s spell it out:
func choose<Result>(
_ condition: Bool,
_ trueBranch: () -> Result,
else falseBranch: () -> Result
) -> Result {
return condition ? trueBranch() : falseBranch()
}
choose(x < y) {
print("true branch")
} else: { // (Swift lets us get away with this! Heh)
print("false branch")
}
SE-0326 is released in Swift 5.7.1, right? Experimenting with what I think (?) is SE-0326 behavior gives fascinating results:
// ✅ compiles
choose(.random()) { 0 } else: { 1.0 }
// ✅ compiles
choose(.random()) {
return 0
} else: {
return 1.0
}
// ❌ error: cannot convert return expression of type 'Double' to return type 'Int'
choose(.random()) {
print()
return 0
} else: {
print()
return 1.0
}
// ✅ compiles
print(
choose(.random()) {
print()
return 0
} else: {
print()
return 1.0
}
)
Would achieving parity with whatever's happening here be acceptable for multi-statement if
expressions?
Edit to add: While the 0
/ 1.0
example above does show parity between choose
and the ternary expression, the other example from the proposal at hand does not:
let x = .random() ? nil : 2.0 // ✅ compiles
let x = choose(.random()) { nil } else: { 2.0 } // ❌ error: 'nil' is not compatible with closure result type 'Double'
…so ¯\_(ツ)_/¯
What does this code mean? In any case I think this usage of yiled
will make it difficult to co-use if
expression in _read
and _modify
.
IMO, yield
is also the wrong keyword to use in case the language ever wants to support coroutines. However, I would support exactly this approach using some other keyword.
I suggested result
as a placeholder up above, which has its own obvious problems:
…but I do think a resolution is lurking in this design space. And we could (I think?) use a word like result
that isn't currently a reserved word, since the context in which this word would exist is not currently syntactically valid.
Although…hmm, accepting this proposal with only single-statement if
expressions would negate what I just wrote: result
or whatever keyword we ultimately want would appear as an identifier inside legal single-statement if
expressions, making the multi-statement migration a breaking change. Yikes. Perhaps another strike against accepting the single-statement flavor as a stepping stone.
For that purpose, I think break value
will also be a possible alternative. In switch
, break
works like return
in functions.
We can reinterpret break
in switch
as one which stops process and pass the value into outside of switch
. And we can do the same thing for if
.
Though break value
does not reads naturally in English, there is no additional keyword and will be understood easily.
Associating break
with if
expressions strikes me as bizarre, since break
as it exists typically appears inside a conditional but breaks from the outer loop:
while true {
let foo = readNext()
if foo < 0 {
break // refers to `while`, not `if`
} else {
process(foo)
}
}
while true {
let foo = readNext()
let _ = if foo < 0 {
break // now refers to `if`, not `while`?!?
} else {
process(foo)
}
}
Still, it's a thought!
+1 to limit the two branch to get same semantics as ternary.
I don't see it as a naming/overload issue — it's that if we start using if
and switch
blocks as expressions, they should consistently act like expressions. We should be able to look at them as replaceable by a function call, just like any other expression you can place on the right-hand side of an assignment.
// this could be an '(Int) -> String' function
let evenOdd = if number.isMultiple(of: 2) { "even" } else { "odd" }
// this could be a '(URL) throws -> FileHandle' function
let file = if let url { try FileHandle(forReadingFrom: url} } else { FileHandle.standardInput }
// no function acts like this
let value = if array.isEmpty { 0 } else { return 3 }
If there are compelling use cases that make this inconsistency worth the potential for confusion, then they should be included in the proposal.
It's not clear that we need to impose that limitation. There’s no precedent for that rule in other imperative languages that support conditional expressions. Swift's neighbors generally either (1) support function returns from condition expressions, (2) only have the ternary operator (or something isomorphic to the ternary), or (3) are functional languages that do not have return
statements at all.
Rust allows it:
fn foo(array: &[i32]) -> i32 {
let value = if array.is_empty() {
0
} else {
return 3 // ✅ returns from function
};
// do stuff with value
}
Ruby allows it:
def foo(array)
value = if array.empty?
0
else
return 3 # ✅ returns from function
end
# do stuff with value
end
Kotlin allows it:
fun foo(list: Collection<Int>): Int {
var value = if (list.isEmpty()) {
0
} else {
return 3 // ✅ returns from function
}
// do stuff with value
}
Scala allows it:
def foo(list: List[Int]): Int = {
var value = if (list.isEmpty) {
0
} else {
return 3 // ✅ returns from function
}
// do stuff with value
}
Javascript, C, C++, Dart, and PHP do not have conditional expressions (AFAICT); they only have the ternary operator.
Go does not even have the ternary operator! Assigning all branches to a local variable is the idiomatic approach.
Python at first seems to impose a limitation like Nate’s proposal. However, Python’s ___ if ___ else ___
expressions are really just its version of the ternary operator: its if
expressions have an entirely different syntax from if
statements, and do not support multiple statements. Python does not have the ?
:
spelling of the ternary operator. (Ceylon is similar.)
The closest thing I could find to a clear precedent for Nate’s proposed rule is Java’s switch expressions:
int value = switch (list.isEmpty()) {
case true -> 0;
case false -> return 3; // ❌ error: return outside of enclosing switch statement
};
I don't have a convenient C# dev environment handy. It would be interesting to check whether it supports int x = boolValue switch { true => 0, false => return 3 };
. It seems likely it follows Java’s precedent.
I agree, but I also don't understand why a multi-statement branch can't be wrapped in a closure. Aren't the following branches all expressions?
var response =
switch utterance {
case "thank you": "you’re welcome"
case "atchoo": "gesundheit"
case "fire!":
{
// Multi-statement branch.
log.warn("fire detected")
return "everybody out!"
}()
default:
try {
// Single-statement branch.
throw IllegalStateException(utterance)
}()
}
or a do statement without catch (perhaps will make the same generated code).
Just to clarify regarding “bizarre” bit - SE-0326 describes why this is happening the way it does - result type is always inferred from first return
and there is no join between multiple return statements, print(…)
example works because Any
is passed down into the closure, so regardless of what each return produces it is always converted to Any
in that case.
Yes, thanks Pavel. I figured it was the presence of a contextual type that made the last one compile again.
It's this step in the chain that really surprised me:
// ✅ compiles
choose(.random()) {
return 0
} else: {
return 1.0
}
// ❌ error: cannot convert return expression of type 'Double' to return type 'Int'
choose(.random()) {
print()
return 0
} else: {
print()
return 1.0
}
(And curiously, both closures have to be multi-statement for it to fail; delete either one of the print()
statements and it works!)
@hborla wrote up above:
…and I agree, so I was surprised to discover that closures apparently already exhibit this behavior!
Fixing one would fix the other because they use the same mechanism under to hood.
I don't believe there are many issues with parsing ambiguities, as the case
keyword allows us to know that we don't have a trailing closure, and ditto for the else
keyword (which an if expression is required to have). There is an ambiguity for an empty switch with no cases, but that doesn't seem like a common thing to want to write in that position, and we'd continue to parse it as a trailing closure. In general, from an implementation perspective we should be able to allow if/switch expressions in arbitrary expression positions, it's just a question of ensuring that SILGen can correctly handle them in those arbitrary positions (I believe it should basically Just Work in most if not all cases, but we'd need to go through and verify it).
It's a syntactic use restriction, so enabling them in other positions is just a question of removing the diagnostic and ensuring SILGen can handle them okay.
I’ll add that
choose(coinToss, ifTrue: 1.0, ifFalse: nil)
has exactly as much as inference ascoinToss ? 1.0 : nil
, just from our rules unifying parameter types. If we accept that today (not at a computer right now) I think it would be fair to sayif
expressions can also do some multi-expression unification.switch
does seem like a step too far to me though.
Personally I would prefer if the rules were consistent across if and switches, e.g it would seem odd to me that:
enum E {
case a, b, c
}
func foo(_ e: E) -> Int? {
let x = if e == .a {
0
} else if e == .b {
1
} else {
nil
}
return x
}
would not have the same type-checking behavior as:
func foo(_ e: E) -> Int? {
let x = switch e {
case .a:
0
case .b:
1
default:
nil
}
return x
}
I proposed allowing
if
/switch
expressions in arbitrary expression position when immediately enclosed in parentheses (further examples here ). Could we make that our first stepping stone? It seems safe enough, and does not present the same mysterious barrier as the current proposal.
Syntactically, this would be fine to allow (indeed it would be fine to allow even without parentheses in most cases). But, as said in my reply to Becca above, we'd need to go through and ensure code generation handles it okay. if/switch expressions as proposed are novel in that they allow embedding arbitrary statements in an expression position (closures are emitted as separate functions instead).
I'm a little surprised to see a specific requirement for
else
spelled out in the proposal. It does seem consistent with definitive initialization, etc, but also causes a lot of seemingly unnecessary burden because in cases where an optional value is expected the developer must now specify both the type and thenil
case of the clause.
I'd like to echo this, though I'd refine @avladimirov's example to:
let foo = if bar { 1 } // fails, incomplete expression returns Int
let foo: Int? = if bar { 1 } // produces .none
I expect the more common circumstances people might run into this is when conditionally mutating a variable:
var foo = 5
// …
foo = if bar { 42 } // replaces: if bar { foo = 42 }
This is admittedly starting to look Rubyish (foo = 42 if bar
), but I kinda like it. Not enough to suggest that we add unless
to the language keywords, but I think incomplete conditionals should probably be allowed when the variable has provably been initialized (or else
can be inferred nil
)
var foo = 5 // … foo = if bar { 42 } // replaces: if bar { foo = 42 }
This is admittedly starting to look Rubyish (
foo = 42 if bar
), but I kinda like it.
Note that Ruby’s behavior differs from what you're describing:
# Ruby
foo = 5
bar = false
foo = 42 if bar # Leaves foo unmodified, but...
(foo = 42) if bar # ...that's only because Ruby parses the previous line like this.
foo = (42 if bar) # This sets foo to nil.
foo = if bar # This also sets foo to nil.
42
end
foo = if bar then 42 end # This also sets foo to nil.
I'd like to echo this, though I'd refine @avladimirov's example to:
let foo = if bar { 1 } // fails, incomplete expression returns Int let foo: Int? = if bar { 1 } // produces .none
At this point this seems very targeted to allowing elision of the else
specifically when dealing with optionals. And personally, I don't think this is a good bit of sugar to add (but then, I'm a bit skeptical of the fact that we allow implicit nil-initialization of optional variables...).
I expect the more common circumstances people might run into this is when conditionally mutating a variable:
var foo = 5 // … foo = if bar { 42 } // replaces: if bar { foo = 42 }
It feels like this is a different proposal to what is appearing here, and in the form of a single if
statement, does not seem worth doing. It's just a different way of ordering the expression, but (unlike with other examples in this thread) no benefits in terms of reducing ceremony – it's purely a reordering. And as such, not useful except as an aesthetic preference.
Now, if you combine it with multiple if else
branches too, it starts to become more than just a simple reordering:
foo =
if bar { 42 }
else if baz { 99 }
else if boz { 123 }
// but no else here – don't perform any assignment in that case
then we are maybe talking about some ceremony reduction.
I'm pretty skeptical this is a good direction – it seems just that bit too subtle to notice (with a long chain) that there is a fall through. I would much rather it always be explicit. You can always write else { foo }
. Or you can go back to the old way, and put the assignments inside the branches – nothing too bad about that.
(And of course this capability would also introduce a difference between declarations vs assignments)
I suspect the best way to decide if this is a good direction is to accept this proposal as-is (I would say that, of course :) and then gain community experience. If there is a strong feeling that, now that we have expressions, this "only assign a new value when these conditions are met" form now really feels like a gap that comes up often, it is easy to add with a follow-up proposal.