This is only a tiny pitch. If someone would like to pick it up and do a formal proposal and implementation, feel free to just go for it.
Having had used the fairly new but rather small Verse programming language for a few month, I started to like the ability to be able to create intermediate constants and variables as well as executing regular functions within the condition-list of the if statement.
I started to wonder why Swift does not have such fairly convenient capabilities. Unlike Verse, Swift does not treat throwing errors as conditions. This is okay and I'm not pitching to change that.
Here are an example of what I would like to be permitted in Swift.
Before:
if condition1 {
var someVariable = ...
// do computation using `condition1` and mutate `someVariable`
callSideEffect(on: &someVariable)
callSideEffect()
if condition2 {
...
} else {
// execute failure branch (mimic A again)
}
} else {
// A: execute failure branch
}
After:
if
condition1,
var someVariable = ..., // NEW (non-condition)
callSideEffect(on: &someVariable), // NEW (non-condition)
callSideEffect(), // NEW (non-condition)
condition2
{
...
} else {
// A: execute failure branch
}
To oversimplify things, treat regular let and vars as non-conditionals. This is a bit similar to how we can create local constants in a results builder, which aren't immediately consumed by it.
This idea would expand the condition-lists of if, guard and while.
What would happen if you specify a conditionless condition-list? A compiler error?
To oversimplify things, treat regular let and vars as non-conditionals.
By regular do you mean non-optional?
TBH I don't like this proposal since I feel like it can only make things harder to read. I've long since started to use , as a synonym for && in these places, and I don't think I am alone. It also feels like it could be easy to accidentally hide a condition among a list of non-conditions e.g.
if condition1,
var someVariable = ...,
callSideEffect(on: &someVariable),
condition2, // HIDDEN
callSideEffect(),
{ ... }
EDIT: I suppose my example is functionally different than what you posted because of short-circuiting, which makes me understand your comparison to result-builders a bit better.
I didn't even know case can do these already. So basically this, but without the need to write case or case _.
I would assume so, the condition list should have at least one condition in it to evaluate which branch it would take in case of if and guard statements.
Fully understandable. However personally I've run into this wall many times where I needed an intermediate constant to extract a value instead of doing it in concrete branch body of an if statement.
Sure one can see this as burying things into the same list, but I don't think it's "that" bad.
It's certainly possible and there are many ways to write things differently, but this paricular example would require extra overhead or boilerplate code, depending who you ask.
It's a fairly lightweight pitch. The community if free to do whatever with it and even reject it. Personally I would love this addition though.
Let's look a the above example of moving the whole condition into a dedicated function again:
Some cons that I personally see with this:
it's not possible to return multiple values
even if it was possible to return values, all will remain immutable and the success branch will not be able to work in such way
While the following example is an extreme oversimplification it's one example that comes to my mind when this could actually shine.
if let nonOptionalNum = optionalNum, test(nonOptionalNum + someNum) {
// we have potentially to perform the same operation yet again as we
// want to use that result
let summedNumber = nonOptionalNum + someNum
}
Instead we could simplify this into:
if
var nonOptionalNum = optionalNum, // condition_1: mutable unwrapped optional value,
nonOptionalNum += someNum, // discardable intermediate computation
test(nonOptionalNum) // condition_2
{
// `nonOptionalNum` is already the sum, no extra operation needed
}
PS: It seems to be possible to achieve using case, but it's not a very great experience.
if var nonOptionalNum = optionalNum,
case nonOptionalNum = nonOptionalNum + someNum,
test(nonOptionalNum)
{
// ...
}
Indeed, that's a lengthier example, but solvable using the same technique, without a new language feature.
func value(_ optionalNum: Int?) -> Int? { // or non-optional but throwing
// in an cumbersome imperative way
guard let nonOptionalNum = optionalNum else { return nil }
let sum = nonOptionalSum + someNum
guard test(sum) else { return nil }
return sum
// or in a concise functional way
return optionalSum.map { $0 + someNum }.flatMap { test($0) ? $0 : nil }
}
if let sum = value(optionalNum) {
...
}
And again it shows how much extra overhead it requires. On top of that, this example only uses a single value for the branch and you even need to wrap the value back into an optional. As soon as you start expanding the number of values you want to use in the success branch, the complexity will quickly ramp up.
Sure things are solvable, and so is error propagation through Result an alternative solution for typed throws, yet we will very likely have both ways. Something that has already a solution does not imply that it doesn't need a different one, especially as it removes unnecessary obstacles. ;)
There would be complications. E.g. foo() returning a generic type, will that work differently if the actual return type happens to be Void vs Int vs Bool vs Optional?
Same question for this one (regardless of whether SomeTypeHere is generic or a non generic type):
we know what the flow would be without looking at the functions signatures (foo() and baz() has to return Optional and bar() Bool). With your proposal the return types could be arbitrary and code would flow dramatically different depending upon the actual return types. This is not a show stopper per se, but will result in a serious language complication.
Personally I wouldn't say it's that complicated as it's already well understood that a condition has to be an optional unrapping or a boolean. So if your function call returns a boolean (like bar()) it will be a condition unless you bind it to a constant or a variable. Returning a discardable optional is not a conditional as optionals require a binding or pattern matching.
@discardable
func generic<T>(_ t: T) -> T { t }
@discardable
func throwingFunc() throws -> Int { 42 }
if
generic(true), // a condition
let value = generic(true), // not a condition
let unwrappedInt = generic(42 as Int?), // a condition
generic(42 as Int?), // not a condition as it does not bind the optional
generic(42 as Int?) != nil, // a boolean condition
generic(()), // not a condition as it returns `Void`
let value2 = try? throwingFunc(), // a condition
try? throwingFunc(), // not a condition as the optional value has not binding
try throwingFunc() // not a condition, error caught by the parent scope
{ ... }
It seems fundamentally weird to have a non-conditional subexpression in a conditional expression. It's harder on readers as then more context is required to understand the control flow. e.g. how does one know that callSideEffect doesn't actually influence control flow? You have to look up its definition - and hope it never changes out from under you to return a boolean.
I fear this might become (if adopted) one of those legacy warts of the language, constantly confusing newcomers. It has that feelโฆ that we'll be forever explaining to people "yeah, that was this thing we did years ago because we thought it was convenient even though it doesn't make much logical sense and it's hard to read, and now we can't remove it because of backwards compatibility".
Additionally, multi-line or otherwise complex if conditions are already challenging to parse (as a human). Some of the alternative forms already shown - such as moving the conditional to a dedicated function - are generally good ideas anyway.
If the main practical issue is the duplication of 'else' path logic - and it's important to short-circuit conditional evaluation - that can be addressed in various other ways too. e.g.:
var allGood = condition1
if allGood {
var someVariable = ...
// do computation using `condition1` and mutate `someVariable`
callSideEffect(on: &someVariable)
callSideEffect()
allGood = condition2
}
if !allGood {
// A: execute failure branch
}
You can do this with case conditions today, using unconditional pattern matches:
if
condition1,
case var someVariable = ..., // NEW (non-condition)
case _ = callSideEffect(on: &someVariable), // NEW (non-condition)
case _ = callSideEffect(), // NEW (non-condition)
condition2
{
Unfortunately let and var as conditions are already taken to mean optional unwrapping, as are bare expressions, so we wouldn't be able to retrofit those syntaxes without adding some type-contextual behavior.
Right, but (a) you have the case prefix to signify that something unusual is going on, and (b) how often does anyone actually use it?
I occasionally reach for if case - typically for some value-binding pattern - but usually end up disappointed because it's difficult to use (especially in if conditionals as opposed to switch conditionals) and it's usually easier and clearer to just move the binding outside the if statement's conditional.
The only time I would recommend doing something like that is if there is a temporary value that depends on a previous condition which is then used in one or more subsequent conditions, as in:
if case .foo(let a, let b) = step1(),
case let c = function(a, b),
c.foo != c.bar {
}
in which case the alternative would be a double-nested condition like
if case .foo(let a, let b) = step1() {
let c = function(a, b)
if c.foo != c.bar {
...
}
}
or something more gnarly if there is also an else clause. In other situations I agree it would be better to factor the bindings out of the condition altogether. But my point is that there is already a syntax to express this, the situations where you absolutely need it are relatively rare, and the proposed alternative syntax is already taken, so to me that makes it difficult to justify a breaking language change on purely aesthetic grounds.