Could we also include "do" in the pitch?
To convert:
let foo: Int
do {
foo = try bar()
} catch {
foo = anotherValue
}
into:
let foo = do {
try bar()
} catch {
anotherValue
}
Could we also include "do" in the pitch?
To convert:
let foo: Int
do {
foo = try bar()
} catch {
foo = anotherValue
}
into:
let foo = do {
try bar()
} catch {
anotherValue
}
Because the first example narrows existing type inference, adding more constraints to a single expression on the rhs (so reducing its search space). Whereas the second does the opposite – it widens the constraint system to need to consider not just one expression, but all expressions that assign to data
, in a way that would inevitably lead to far more "expression too complex" problems, as well as very hard-to-reason-about inference behavior.
Yes, you can simulate this today by wrapping the if
in a closure, and using type inference. But even then, at least it constrains the problem to code within the closure that must be the rhs of the declaration. Placeholders for later statements imply the type can be determined at any point later in the function, essentially turning the entire function into a constraint system.
I don't think it would cause any breaking changes. Everything that is annotated with a builder won't use the new if and switch expressions and continue to use the builder transformations. It's the same with existing single value expressions and buildBlock
. For instance, in this example the builder "wins" over the implicit return and returns 0
:
@resultBuilder
enum ZeroBuilder {
static func buildBlock(_ components: Int) -> Int {
0
}
}
@ZeroBuilder
var number: Int {
1
}
print(number) // -> 0
It's not especially compelling since this is let foo = try? bar() ?? anotherValue
. It becomes a lot more compelling if/when we figure out the multi-statement story. So I'd be tempted to put it into the "future directions" bucket.
(more generally, personally I'm a bit skeptical of try?
/try!
except for throwaway code/samples etc... you should probably do something with errors that are important enough to thrown, and so encouraging another way to just discard the error and sub in a default value, without multi-statement expressions where you could at least log then return a default, seems unwise)
In response to Paul Cantrell's observation on Java
var response = switch (utterance) {
case "thank you" -> "you’re welcome";
case "fire!" -> {
log.warn("fire detected");
yield "everybody out!";
};
}
To unpack this a bit, Java's switch has two changes (aside from String matching):
->
instead of ':' to mean this case statement (like Swift's) requires no break to close the block, and it must return a value (expression).yield
to disambiguate multiple statements.I find the ->
very helpful to signal the intent of the case statement, like let
in
case let .valid(scalar): return scalar
I would make ->
a requirement for any case statement used as an expression, to avoid conflating it with block case statements.
As proposed, the rules for case-expressions are quite restricted (relative to case-block's, for good reason), and I think it's much less confusing for the reader to indicate that overtly.
The ->
is like a function return type declaration: the result of anything from this scope.
I also suspect ->
will help the compiler know immediately it's in expression context.
e.g.,
public func progress() throws -> Float {
// return here is optional in single-expression context, but clearer IMO
return switch state {
case .starting -> 0.0
case .running, .finishing -> self.buffer / self.unit
case .completed: -> 1.0
}
}
I prefer the ternary form for short expressions too. "if" would only make sense for long expressions where the ternary operator is often abused.
I think it would be useful even in under single-expression rules to permit return and throw statements to apply to the surrounding closure or function.
I think that means return
should NOT be used to return the value of the expression in an assignment.
From the original proposal (emphasis added):
if
and switch
statements will be usable as expressions, for the purpose of:
return
);The proposal says usually return
can be considered ceremony:
public static func width(_ x: Unicode.Scalar) -> Int {
switch x.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default: return 4
}
}
Permitting throw
and return
to exit the function scope supports fast-path and fast-fail uses for case legs. e.g.,
public func progress() throws -> Float {
let buf = switch state {
case .starting: return 0.0
case .running, .finishing: self.buffer!
case .canceling: throw .err("reentrant")
case .completed: return 1.0
}
let unit = ProgressKit.findUnit(buf, self.config)
return ProgressKit.progress(buf, unit)
}
I favor the first line (instead of last line) of multi-line blocks being the implicit return so it doesn't break the flow of the outer expression. Swift could introduce a "where" keyword (or “with” as an alternative) to blocks like the equivalent to where in Haskell. Functional programming languages generally use a style of putting the result expression on top and dependent expressions after. This would be a variation of that, albeit with side-effects. I think this would fit in better when using a functional-style in Swift. Additionally, this avoids surprise since all implicit returns are obvious. This syntax would prevent using implicit returns when using an imparative style (like Swift style guidelines currently recommend) which I think is great.
let bar = switch foo {
case 0: 1 + 2
case 1: fallthrough
case 2: a + b where // maybe “with” instead of “where”
let a = 1
let b = 2
default: a + b where
// optionally use break to return explicitly (a labeled break should work too)
guard Bool.random() else { break 42 }
someFunction()
let a = 1
let b = 2
}
let bar = if foo > 5 { a + b where
let a = 1
let b = 2
} else { a + b where
someFunction()
let a = 1
let b = 2
}
// Maybe generalize for all expressions
let bar = { a + b where
let a = 1
let b = 2
}
var foo = false
let bar = { a + b where
foo = true
let a = 1
let b = 2
}
let bar = do { a + b where
let a = try fetchValue()
let b = a * a
} catch {
print(error.localizedDescription)
}
// Maybe functions and getters too
func f() -> String {
if b == true { "true" } else { val.lowercased() where
let val = "FALSE"
} where
let b = Bool.random()
guard b != self.lastB else { return "last" }
}
func f() -> String { "TRUE".lowercased() where // called second
someFunction() // called first
}
var foo: String { b == true ? "true" : "false" where
let b = Bool.random()
}
// leaving out expression allows avoiding implicit return
let closure = { where someFunction() }
EDIT: Edited a bunch of times to flesh out example.
To be clear, I was not proposing anything more than lifting Java’s idea of using a return
-like keyword that is not actually return
in multi-statement branches of conditional expressions. But yes, there are some things to be said for ->
as well!
Java’s practice of using both :
and ->
with switch/case can be confusing in the learning process. I wonder whether they chose to introduce ->
only out of parsing necessity, or whether it was specifically meant as a “this is an expression!” clarification for code readers.
(And for the record, I’d prefer this direction if it proves workable:)
Unless throwing bar()
has an optional return value , or you want to use an error somehow:
let foo: String = do {
try bar()
} catch {
"Error \(error)"
}
Indeed:
let foo = do {
try bar()
} catch {
print("error: \(error)")
return anotherValue
}
Removing the braces from the if statement is one step in the direction of unifying it with switch. In your example, you use ->
but switch uses :
. If you use :
too, you converge on switch
even more. Maybe we only need one?
Interesting perspective. I remember back when Swift was new I wondered why use ->
instead of a colon:
func foo(): String {
if isRoot && (count == 0 || !willExpand): ""
else if count == 0: "- "
else if maxDepth <= 0: "▹ "
else: "▿ "
}
typealias T = (Int) : Double
I think it comes from the established mathematical notation for functions:
f: U -> V // U is the domain of the function f, V is its range
I actually prefer the opposite (implicit return on top) so it doesn't break the flow of the outer expression. I put a more detailed example in another post.
let bar = switch foo {
case 0: 1 + 2
case 1: a + b where // expression blocks have result at top
let a = 2
let b = if Bool.random() { a + c + d where
let c = 42
let d = Int.random(in: 0...a)
someFunction()
} else { 9 }
default: a + b where
someFunction()
let a = 1
let b = 2
}
I'm very excited to see this proposal! It's been a thing I've continiously missed a lot ever since moving to Swift from Scala! It really helps in writing nice functional code and removes a lot of boilerplate / noise.
It also feels very natural in Swift, which already has set one foot (a whole leg perhaps...) in the door with both var x: Int { 12 }
as well as function builders.
People on the thread have already mentioned their experiences with Rust or Ruby which allow for such things, but I think we can learn a lot from Scala here -- and avoid mistakes and pitfalls it's "everything is an expression" has. Especially since Scala has a rich and strongly typed type-system and is on the other side of the spectrum than Swift wrt. the "what is an expression" question.
I'll go with a few examples and discuss the pitfall, and how we address, or could potentially address it:
In Scala it is possible to only write the
if
without else and it still is an expression:
// scala
val x = // Unit (!)
if (randomly) 12
An
if
without an else has a type of Unit
. Which is not wrong but it can be a bit weird. So generally you'll want to have an if/else always if using if as an expression:
// scala
val x = // Int
if (randomly) 12 else 42
Swift: Good, the current proposal handles this already -- we're saying an
if
must have an else
in order to be an expression! I think that's the right call here Rather than leave the Void inference, we can make people who want to use an if as an expression always provide the alternative branch.
In Scala, different branches may have any type they want, and the type of the if expression is the shared common supertype. For example:
// scala
trait Top
case class Left() extends Top
case class Right() extends Top
def example = // : Top with Product (Product because they're case classes...)
if (randomly) Left() else Right()
in this example this works nicely, we get the Top
type. But you'll notice that we're leaking additional information by accident: the with Product
in the type is accidental from the fact that both Left and Right are product types (case classes), so we leaked information here by accident.
So again, being explicit about the type to be returned by an if/else expression is a good pattern anyway -- so if we have more cases where we have to do this in Swift, I think that's no big deal as that's a best practice anyway in other languages.
This often comes in handy, say when I'm returning an existential "any Top
" as we'd call it in Swift:
// swift
func make() -> any Top {
if randomly {
Left()
} else {
Right()
}
}
I would certainly like the ability to do this. I was not entirely clear if this is proposed to work or not. The proposal states:
// invalid: let x = if p { nil } else { 2.0 } // valid with required type context: let x: Double? = if p { nil } else { 2.0 }
which you may have noticed I believe is good anyway. Specifying the type there is useful, even for local declarations.
Question: Under this proposal, can I write the make() -> any Top
function with a single expression, or would I need to as any Top
or something in the branches?
I think we need to look at an example of how "last expression is the return" affects programs. In Scala, it is effectively an anti pattern to use return
since it can be surprising, so instead code is written in order to always have one "last expression".
Look at this real example from Akka's build:
@tailrec
def findHTMLFileWithDiagram(dirs: Seq[File]): Boolean = {
if (dirs.isEmpty) false
else {
val curr = dirs.head
val (newDirs, files) = curr.listFiles.partition(_.isDirectory)
val rest = dirs.tail ++ newDirs
val hasDiagram = files.exists { f =>
val name = f.getName
if (name.endsWith(".html") && !name.startsWith("index-") &&
!name.equals("index.html") && !name.equals("package.html")) {
val source = scala.io.Source.fromFile(f)(scala.io.Codec.UTF8)
val hd = try source
.getLines()
.exists(
lines =>
lines.contains(
"<div class=\"toggleContainer block diagram-container\" id=\"inheritance-diagram-container\">") ||
lines.contains("<svg id=\"graph"))
catch {
case e: Exception =>
throw new IllegalStateException("Scaladoc verification failed for file '" + f + "'", e)
} finally source.close()
hd
} else false
}
hasDiagram || findHTMLFileWithDiagram(rest)
}
Ignoring what the code does -- you'll notice it is pretty nested because it strives to follow this expression return style.
The entire method is an if
wrapped thing:
if (dirs.isEmpty) false
else { LOTS OF LOGIC HERE }
Now... this is where things become weird with Swift, since it has a great facility to avoid such nesting -- guard
! So in Swift I'd argue the preferred style would be:
// WARNING: weird pseudo-code "translation" to what-if-swift
func findHTMLFileWithDiagram(dirs: [File]): Boolean = {
guard dirs.nonEmpty else {
return false
}
let curr = dirs.head
let (newDirs, files) = curr.listFiles.partition(_.isDirectory)
let rest = dirs.tail ++ newDirs
let hasDiagram = files.exists { f =>
let name = f.getName
if (name.endsWith(".html") ...) { // THIS IF IS RETURNED
let source = scala.io.Source.fromFile(f)(scala.io.Codec.UTF8)
defer { source.close() }
let hd = // NOTE: removed the try/catch, will discuss below
source
.getLines()
.exists { lines in ... }
hd // RETURN: this is a bool, and initializes hasDiagram (!)
} else false // RETURN: (else branch) initializes hasDiagram
}
hasDiagram || findHTMLFileWithDiagram(rest) // RETURN: method r
}
So... here's where things grit a little bit... in such code the styles are mixed and that leads to confusing code. We like Swift and we like guard
, so we'll likely be writing guard whenever we can -- but that is the "early return" style, that is in opposition to the "everything is one expression" style. When the styles are mixed, we get confusing code... (as in this "pseudo"-swift translated example.
I'm myself not entirely sure on the complete answer here... but perhaps this is hinting at the fact that Swift should not strive towards this "one huge expression" style, because of the existing language tools are well geared towards early returns. And that perhaps implies that we should limit where and how we use this expression return style...
Summary: All this introduction is to showcase the conflict I'm having with regards to allowing multi-line expressions inside here or not... If we DO allow, then we're leading towards this "everything as an expression" style, which does not mix well with existing patterns. BUT, not allowing it is also incredibly annoying:
For example, disallowing multiple lines of code in the if/else expression may cause the example used in the proposal to be incredibly annoying IMHO:
let x =
if randomly {
log("yes")
12
} else {
42
}
I can definitely see myself be really infuriated every time I try to debug some Swift, and have to wrap things into the {}()
dance only because I needed to add a log statement somewhere.
Summary summary: I'm a bit worried about mixing styles, but the pain of allowing only single-line expressions I can definitely feel would drive me very annoyed when working in the real world (using mild wording here ).
In Scala the return type of a method can be omitted and inferred . This is a pitfall more often than not, a lot of the "bad examples" I'll list below kind of fall back to this issue, so remember that this is legal:
// scala
def example = 12
// is the same as
def example = { 12 }
// is the same as
def example: Int = { 12 }
// is the same as
def example: Int = { return 12 }
// a "void" returning method is:
// def example { 12 }
// or
// // def example: Unit = { 12 }
as you can imagine, this can lead to issues, when declaring APIs. However it is nice for overriding/implementing methods, since you don't need to spell out the type, but the requirement is still there from the being overriden method...
Either way, Swift does this right by forcing the return type be written always so we don't run the same risk. Nothing to be done here, we're good from the get-go.
Switching / matching is really just a nicer
if
as far as rules concerning type inference etc are concerned. Nothing much to add here really.
In Scala try/catch
(our do/catch) is also an expression:
scala> try 12 catch { case _ => 42 }
val res1: Int = 12
but as the proposal states, this is equivalent to
try? ... ?? alternative
which is a more concise form of the same (though without the power to handle the error). So while it may be nice to add in the future, I don't think we're losing much here.
Arguably though, for consistency's sake it may be a future extension that's worth exploring... Especially since such expression do/catch can also perform Error -> DefaultValue
depending on the error` conversions... this isn't super common though.
I think the shape and scope of the proposal, as presented is good - we're avoiding problems Scala has with those expressions, and we have more room for growth in the future.
I, personally, would still want to see multi-line support, just because I know it will be incredibly annoying to write nice beautiful code with expression style, and then debugging it and adding logging will be a nightmare otherwise making it all horrible and ugly again. As a community though, we should embrace the multiple return style that guard
and other pieces of the language are good about. This means not shunning the return keyword as much as Scala does for example -- it still has its place, I suppose, in Swift.
Hope this was at least a bit helpful to see how we stack up against other languages with this feature
I am pleasantly surprised to see this pitch! I think up this is a great addition to the language, and I think it would be a good thing for inline if
-else
to replace ternary expressions.
I share others’ concerns that implicit return of final expressions is a bridge too far. If nothing else, it risks breaking existing Swift sample code in surprising ways. It’s also really dangerous to accidentally leak information from a function. I quite expect that adding this feature would result in at least one CVE.
I’m not sure I believe that CVE-worthy information leaks become drastically more likely with generalized final-expression returns than they are with what’s being proposed here, but okay. If we accept that final-expression returns are evil, but only-expression returns are not, can we at least avoid the code evolution problems of the current proposal by saying that you don’t have to uniformly use only-expression returns in a function? That is, each individual path can decide whether to have an only-expression return or a normal return
after multiple statements, so that adding a second statement in one of the blocks only means adding a return
keyword to the following expression instead of either completely rewriting the function or surrendering to {}()
within what’s already a block.
What kind of breaking changes do you expect? I don't think any code that compiles today would behave differently if implicitly returning of final expressions would be introduced.
I initially thought that maybe overload resolution based on closure return type might be a problem, but there seems to be a precaution in the compiler that prevents implicitly Void
returning multi-expression closures from being used for overload resolution.
func launchRockets(_ preconditionsMet: () -> Bool) {
if preconditionsMet() {
print("continue")
} else {
print("abort")
}
}
func launchRockets(_ alwaysAbort: () -> Void) {
print("abort")
}
launchRockets { // error: Ambiguous use of 'launchRockets'
print("accidentally trigger void overload")
true
}
I'm seeing it like others in that we have already halfway arrived at implicit returning of final expressions and that the current state feels rather inconsistent. For the subset of Swift syntax that is allowed in result builders it's even possible to make this work in todays Swift:
@ImplicitReturn
func isZero(value: Int) -> Bool {
print("value: \(value)")
if value == 0 {
print("zero")
true
} else {
false
}
}
@resultBuilder
enum ImplicitReturn {
static func buildPartialBlock(first: Void) -> Void {
()
}
static func buildPartialBlock(accumulated: Void, next: Void) -> Void {
()
}
static func buildPartialBlock<Value>(first: Value) -> Value {
first
}
static func buildPartialBlock<Value>(accumulated: Void, next: Value) -> Value {
next
}
static func buildEither<Value>(first value: Value) -> Value {
value
}
static func buildEither<Value>(second value: Value) -> Value {
value
}
static func buildArray(_ components: [Void]) -> Void {
()
}
static func buildArray<Value>(_ values: [Value]) -> Value? {
values.last
}
static func buildOptional(_ component: Void?) -> Void {
()
}
static func buildOptional<Value>(_ value: Value?) -> Value? {
value
}
}
I was wondering this to myself while reading this thread because on the one hand it was how I automatically assumed it would work, but then I realized that my assumption was based in the new perspective that this if/else statement is expected to produce a value. Then I remembered that return statements in if/else branches are already a thing with a very different meaning, and that maybe that's why we can't use return in this way. At the moment I'm feeling favorably about the idea of a different keyword for yield
ing a value towards a composite expression as opposed to return
ing a value from a function.