Thinking about this some more, this is actually not ambiguous, due to the fact that if condition 1 else 2 is invalid [1]. So yes, this is actually a workable solution.
Um... What? A function returning Void in a functional language would basically be an expensive no-op, since functions are not allowed to do anything other than return a value. They can't modify values, or globals. They can't have any side effects.
Even in "unpure" functional languages that allow the occasional function with a side effect, those would be the exception, not the rule. They would be inherently rare.
Are you sure you're not confusing returning Void[2] with returning nil[3]?
Hmm? Odd. I distinctly recall that working. Or maybe it's only in closures:
@discardableResult
func confusinglyInt() -> Int {
return 1
}
let implicitlyVoid = {() in
print('Void')
confusinglyInt()
}
let implicitlyInt = {() in
print('Int')
return confusinglyInt()
}
let unintentionallyInt = {() in
confusinglyInt()
}
It’s still a type though (Unit), without any info. Usually used to show side effects, but still can be returned in “pure” cases to omit values, like [()]—array of no info needed.
Btw, as shown in your example Void is already a special case–you don’t need to write return for regular functions in the end, but can like ‘return print(“hello”)’
Not sure I’ve understood here, closures and not that different. You just can’t, obviously, omit Void for them.
Ah, my apologies; I think I misunderstood your point.
Regarding the rest of this thread... I've honestly been coming around to being -1 on having the last expression as a return value. I think reading through this thread demonstrates that it would make for more of a mess, not less of one. If Swift is going to address this situation (and I would argue it doesn't necessarily need to), I would much prefer adding a new keyword of some kind, whether that be a scoping keyword like let x = frungle { y(); z }, or something like let x = do { y(); then z }. To me, that makes it just an additional thing to learn in a new context, rather than a new bundle of exceptions to have to continuously think about when they do and don't apply to a given situation.
My impression is different. There seem to be some misunderstandings around basic features of Swift, I have not seen any real showstoppers here. Besides this I think it boils down to personal taste.
There is a lot of personal taste involved in creating and evolving a programming language, and this is what I mean. The other question is indeed about whether people should have the choice of putting a “return” or not at some places. They already have choices like this today, and the pitch is about whether this should be expanded to the places described in the pitch.
Yep, though afaik (haven’t touched in a while) in kinda only ‘pure’ Haskell it used not quite often but still you can use it to omit values, in last languages I’ve checked like Scala or Ocaml it’s used frequently, especially for side effects. It’s actually quite important type, even for Haskell. Thus my comment—it’s used, I don’t disagree though the usage is a bit different. though I’d argue Swift has functional style Void, not C like.
I think it’s ok? Still don’t see how it’s correlated to pitch.
Personally for me those examples still not showing real problem. Result builders being answered by Ben, one could argue about solution though. Rust example is quite readable (for me), so maybe someone can come up with a better example. And large function is basically a side effect problem and it doesn't matter where you put a function, it can break logic.
Now for @discardableResult—this is concerning, I do see that could be a problem, especially in codebase where you work with frequently used types (e.g. String, Character and etc.). @ben-cohen in pitch there is an example for @discardableResult:
@discardableResult
func f() -> Int { 1 }
let outer = { () -> (() -> ()) in
// return value of inner closure is left inferred
let inner = { () in
// statements that prevented this closure
// from being single-expression, inferring ()->Int
print("do something")
// discardable result, so no warning when
// this closure previously was inferred as ()->()
f()
}
// With -enable-experimental-feature ImplicitLastExprResults this now fails with
// Cannot convert value of type '() -> Int' to closure result type '() -> ()'
return inner
}
If we change closure to return same type as last function—could we show a warning that return function is marked as @discardableResult but it's value is used, like:
let inner = { () -> Int in
// statements that prevented this closure
// from being single-expression, inferring ()->Int
print("do something")
// discardable result, so no warning when
// this closure previously was inferred as ()->()
f() // Warning: Function is marked as a @discardableResult, but its value is used as a return
}
As demonstrated just two posts below yours, if this pitch is accepted, cases where a closure's return type was inferred could suddenly infer something different, forcing a change to specify the return type. Hence, how natural or unnatural the syntax for specifying a return type for a closure is, is relevant to the implications of this pitch.
The alternative is to demand, for backwards compatibility, that @discardableResult as last line would be treated as discarded if the return value is inferred and there's no explicit return.
How it behaves when the return type is specified could be open to discussion: Assume discard, or change behavior to match the return type (Void or other).
I think the major problem with examples is that they are a bit out of context. Clearly, looking at 30 lines of code in an excerpt here makes them look easy to digest. But put this in a context of a large project, and this is how I try reason - mapping usage to everyday cases - where code not that pretty and surrounded by tons of other related stuff, and it becomes much harder to comprehend. And you are spending mental capacity on that anyway, and for me the less energy is required to understand syntax, the more I have for the actual task.
Does this the reason to dismiss this case from consideration and impact of implicit returns there? Large functions initially have issues in comprehension, that’s right, yet this is the reality for a significant part of the code after all - it’s not a side effect problem (unfortunately).
I’d argue that the change promotes writing larger functions, because if currently you will think about moving some switch into a separate function to just return from there as there is no alternative, this changes makes it possible to leave it part of the function, increasing size of an arbitrary function. This actually true even for how expressions work currently, but limitations and general pros for them outweigh cons, which seems to fade out when we allow side effects in this expressions happen.
I think the whole idea of extending expressions like if and switch in the light of introducing side effects to simple expressions isn’t that good. Initially I’ve been missing this abilities, but with more thoughts put in, allowing multiple statements makes them not that simple, deserving more attention and structure. With the current state of affairs one is more likely to extract a function, leading to the better code and intent expressed with it.
To me the real pain point is actually { … }() - if that could’ve been replaced with do-expression, I think we would’ve covered the majority of cases and addressed this closures pain that currently is used instead. Consequently, this would allow more complex if and switch expressions, just because now do expression can be used in a branch to execute several statements. And the major distinction I see in that approach is that now you have to explicitly reach for this feature.
Upd.
Ah, got now. You mean with new proposal last one would be () -> Int in the end for unintentionallyInt ? Guess compiler will just fire an error if it is used differently.
Context matters, that's overall problem with large functions in imperative languages, regardless return keyword, not sure it could be counted, end of function easy to check anyways.
Still think it's an overkill:
If you want to add just 1-2 lines why not to adapt semicolons to sequence expressions? Like
let width = switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default:
print("ok");
log("this is unexpected, investigate this");
4
}
If there is a bit more complex logic closure is already a solution:
let width = switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default:
{
calculate(); print("ok"); log("this is unexpected, investigate this")
return 4
}()
}
What I like about closure it's explicitly showing you side effects. What I don't like about second example and agree with you is:
Apologies to sound like a broken record, but would anyone mind to help me understand why that in and of itself is a serious pain point? If we are going through this trouble for single and multiple line expressions is to avoid it… but why? What is the big nasty problem they caused and the better things we can do only because we have an alternative to that {…}()?
Happy to take it to direct messages, it is really asking for help here hehe :).
The question is jarring to me, because it’s implying that those who can „handle“ implicit returns are fine with them and those who cannot are not.
The same argument could be made about braces surrounding if branches. The point I’m trying to make is that it’s fine until it isn’t. The benefits don’t even come close to outweighing the downsides IMO.
A -1 for me, mostly on the basis that I found this greatly confusing every time I had to work with code written on a language that supports this, because I have to double check every function to determine what's actually being returned in each case due to the lack of an obvious return statement.
@discardableResult functions already have their values implicitly returned from single line closures. Forcing them to be implicitly discarded from non-void multi line functions where the last statement matches up with the return type would not be backwards compatible, but I can certainly understand adding a warning that the function is not in fact being discarded when used in an untyped closure, and list the three ways in which that could be rectified:
Typing the closure or the variable it's assigned to if it's immediately executed.
Explicitly discarding the result of the function.
Explicitly returning the result of the function
Personally, I think that @discardableResult was a well meaning, but ultimately incorrect, decision to generalize the ability to implicitly discard functions returning the empty tuple and it's a shame that we're stuck with it.
I certainly don't find @discardableResult functions being hard to work with a reason to reject this proposal, as they already are hard to work with.
I am willing to live with either break {label}: value or using semicolons ala Rust as a compromise, but I would hate to lose Swift's lovely lack of end-of-line punctuation.
What is the big nasty problem they caused and the better things we can do only because we have an alternative to that {…}()?
It adds two extra lines, it adds a level of indentation, it introduces a closure scope so you won’t be able to return from the outer function for example, and it’s ugly.