Enhanced 'do' Block Syntax and Error Handling

I'm having some trouble in Swift these days when Control Flow blocks and expressions get the same name, telling apart when when I'm doing legit Control Flow and when I'm in a multi-line expression with an implicit(!!!) return.

The most recent was my confusion was with result builders that the "for" in that is really more of a foreach {}

So all of a sudden do and catch will be sometimes be expression and sometimes Control Flow?

I don't mind having both CF and expression but how do I know which I'm doing if they have the same name? Maybe a "doSet" and "catchSet" would be okay? But people seem to really want the same names for these very different creatures? That's the part I don't understand.

Is this just the way modern languages do things? Could someone point me at a paper or a youtube video or give me some search terms so I can educate myself on why everyone hates control flow programming and wants expressions to overload those terms? Is it just "functional programing" and I just need to do it? I get the value of no side effects... but then on the other hand we have people adding ~Copyable and borrow to the language too, which seem all about a return to side effects, really.

Honestly, I'm flexible and can learn so I defer to what the community wants but I'd love some resources so I can understand the debate better.

Ah, the ship seems to have sailed and I'm just going to have to write C++ libraries now :joy: :wink: [Accepted with modifications] SE-0380: `if` and `switch` expressions (This is very much a joke. I will enjoy not writing return in my switch statements with everyone else!)

1 Like

Nobody hates imperative style in Swift so these papers don't exist. There are opinionated libraries for Swift that might choose to more strongly embrace either functional or imperative styles, but the core language tries to be un-opinionated. I certainly prefer one or the other depending on what feels better for the task at hand.

There isn't a contest to make functional programming take over Swift or anything like that. Swift was designed to be multi-paradigm from the beginning. I think there is a the desire to use the same constructs across the different supported programming language styles to avoid eating keywords and to make it more familiar. Both styles are considered equally relevant. Rust is very similar to Swift in regard to treating both equally.

As far as what common practices will be, that is just going to be determined by how people use Swift in real life. You might find that there ends up being a lot of consensus to use a functional style in some cases and an imperative style in others, but that is always going to be in constant flux as it was meant to be. The only exception might be the few opinionated libraries that Apple releases (i.e. SwiftUI) that will be determined primarily by Apple.

Most of the time the goal for library building is to make them un-opinionated, but there are exceptions if you are in a domain that strongly prefers one or the other. I would certainly want to use imperative style if I were building an interpreter and functional style for a mathematical calculation. Something like list processing is in a middle ground where either might be better.

Most modern languages with advanced type systems have chosen to be multi-paradigm or functional since that is where these type systems originated. Even looking at most modern languages in general there is a push to functional style now that so many people have used JavaScript or TypeScript. Of recent languages Go might be the biggest outlier which ended up strongly imperative, but with a very limited type system.

As for the implicit return. It doesn't exist for Never/Void types, so you just need to pay attention to return types. It also should be used for expressions only, so multi-line implicit return should be unsupported or rare. As a strongly typed language, in virtually all cases the compiler should catch accidentally doing an unintended implicit return. In your own code, if you are using an imperative style I would always do an explicit return. Save implicit returns for code that is purely using expressions. The core team is deliberately adding expressions to block constructs slowly to make sure imperative style isn't affected because it is important too!

2 Likes

Thank you for your reply.

As I belatedly realized, the ship has sailed on overloading block names as expressions, so I apologize for derailing the conversion. Now that I realize if/switch are (edit: going to be) expressions, I withdraw calling a do expression something more decorated. Consistency is more important.

I use control flow blocks while also using functional paradigms (eeeek? Apologies.) so I would gently push back on the implication that OVERLOADING the block names doesn't make programming harder / code more ambiguous. Its not about imperative vs functional styles exactly as much is about the existence of any flow control statements in a project that might be functional in general but not religiously. But as you said, and as the powers that be have said, that's just going to be the "way things are done". And I will be fine. I'll just have to learn to look for equal signs? Maybe I'll have XCode make them a bright color or something.

Just please please, no multi-line expressions with no return/yield or something. That makes it the hardest to parse if its an expression vs block when scanning the code. From SE-0380 it looks like those are for one liners and multiline hasn't been decided yet and its really just the for in result builder that is a special (uniquely problematic to me) case, so this is my one ask :blush:

I will say from a personal experience when I started using Swift it felt like easy-gracious-beautiful C/C++ and now it kinda feels like... not that. It's becoming its own thing. I just haven't been following evolution stuff previously and am trying to get a feel what the philosophy is so I can not be so taken by surprise when things don't work the way I expect/change. :pray:

Well I don't think there will be confusion with implicit returns with the restrictions the core team has (or will as it is expanded) put in place. Also many of the examples in this thread are assuming implicit return from multiline expressions will be allowed which I'm almost positive there will never be consensus in the Swift community for that to ever happen and the return keyword can't be used to return from a block.

So I think what you might be most lamenting is an expression in the middle of imperative code which is already common in the Rust language.

// forgive the nonsense example...
for a in b {
  var c: Int = a
  // This is pretty much the main situation where imperative will mix with functional. 
  let d = if a > 1 { 1 } else { 2 } 
  c = c + d
}

Because of higher-order functions like map, Swift programmers are already heavily mixing functional and imperative styles. This really just takes that one step further.

I am personally fine with it as long as the lines don't feel too blurred. I think it is still pretty clear there are boundaries. If you don't like it then avoid mixing styles in your own code. Not sure what else to say about that...

It's not really question of what I do in my own code, its a question of scanning other peoples mixed paradigm example code while only paying half attention. :wink:

btw, no way my earlier example would actually fly due to multi-line blocks.

Probably something like this, but I don't think there is any consensus yet on how to terminate multi-line expression blocks which probably needs to happen before any kind of pitch like this can move forward.

var bar: String = do {
   try fetchString() catch { throw InnerError.fetchOperationFailed }
} catch { // catch in the outer do
   print("Today is not your day.")
   insert_keyword_here "" // probably break, yield, etc.
}

agreed. this is something that pointfree tends to push hard, but it is not something the language itself pushes.

2 Likes

(based on what @esummers brought up earlier)

  guard let result = try fetchString() catch { 
    throw InnerError.fetchOperationFailed 
  } else { 
    throw InnerError.noResult 
  }
  guard let result = try fetchString() else { 
    throw InnerError.noResult
  } catch { 
    throw InnerError.fetchOperationFailed 
  }

Only the first version should be permitted. Putting the catch after the else creates ambiguity about whether it covers the contents of the else block (and I don't think it should).

It should also be explicitly stated that when used in a guard statement the catch block is subject to the same requirements as the else block re. having to cause execution to exit the surrounding function.

Even given that, I do find it a little awkward to reason about the control flow when used with guard like this, because now you have three paths but only two of them are really obvious (those with explicit blocks, the catch and else). Ironically the happy path - no exception and a non-nil result - is the hardest to follow. It's possible that this is just unfamiliarity and that with exposure to this syntax it'll become intuitive.

3 Likes

Actually, I have no idea what happened to that example. The second one is clearly messed up, but I think you got the gist.

Sorry, I was so fixated on the order of catch vs else that I missed the other differences. I edited the examples in my post to more clearly frame it around the point of interest.

Honestly, in the specific case of multi-line do/catch, my biggest concern is that its really easy to miss that the catch is proceeded by a do() expression and not a do: block.

Something like the example you gave above is a perfect example of a miss-able assignment if you're just scanning the indents:

//more code here
//more code here
var bar: String = do {
  guard let result = try fetchString() catch { 
    throw InnerError.fetchOperationFailed 
  } else { 
    throw InnerError.noResult 
  }
} catch { // catch in the outer do
  print("Today is not your day.")
  ""
 }

seems kind of like a logical mess to scan quickly if one has ever even used a block expression do-catch.

It's a similar concern to what I'd have with multi-line if/else()

With a one line do/catch or if/else, the exit or second expression will be reasonably close to the call, but in a multi-line do or catch it would not.

If you can be confident the code base only uses one or the other as maybe a linter enforced rule, I mean sure. Great no worries. But I don't know a lot of code bases that are that strict? And I don't generally read the linter rules before I read someone else's code, but perhaps I should.

I suspect throwing a bright pink return or other keyword at the bottom of those closures would make it harder to miss? Would that be acceptable in purer functional circles or a travesty?

Technically this is just a syntax thing and not an imperative vs functional thing. The same issue would occur in imperative style. I think you just need to get used to this in Swift since there are a lot of similar situations. Also this is a bogus example, not something you would see in real life.

What I'm hearing that a return or yield or other keyword on exit does not feel like an acceptable compromise to multi-line blocks-as-expressions to you?

FWIW, I like some of those syntax suggestions you deleted. I guess you didn't in the end!

I'm not sure what those were, but I personally feel that multi-line expressions should not be allowed to be an implicit return and require some other way to return from the block with a few exceptions. Namely guard statements should be allowed. I think this is the majority opinion and I'm in agreement with that.

The main reason multi-line implicit returns are allowed in function bodies are for Result Builders, but I think Swift should rethink that situation.

I actually like implicit return when it is clear something is an expression though...

1 Like

Result builders are extra weird because if evaluates like a block and the for like a multi-line expression with an implicit return. Hence the saltyness about overloading of block commands as expression. I understand now that mult-line implicit returns are not what most people want for the main nonDSL/AST part of the language :+1: :+1: :+1: That works for me. I will in fact just get used to the overloading.

Some of the proposed code examples (i.e. the #2 do {}, some of the do/catch statements) have multi-line implicit returns, so I gotta vote -1 in the mean time.

1 Like

Hmm, I missed this the first time, but prompted by @carlynorama's comments, I realised the above isn't valid. The main block of the do isn't actually returning a value, to assign to bar. guard is a statement, not an expression - it does not function as an rvalue.

The fix is of course a simple return result after the error-handling paths.

I'm not a fan of the implicit return in the catch block. Perhaps that's orthogonal (implicit returns in general)… but it does feel a bit more 'magical' (in a bad way) than normal in this case. Perhaps implicit returns shouldn't be permitted in these catch blocks - just like they're not in guard's else block - to ensure the author makes a clear choice.

Given that, the example would become:

var bar: String = do {
  guard let result = try fetchString() catch { 
    throw InnerError.fetchOperationFailed 
  } else { 
    throw InnerError.noResult 
  }

  return result
} catch { // catch in the outer do
  print("Today is not your day.")
  return ""
}
1 Like

Correct there are typos. I have to agree that I’m not a fan of that catch block either. Muscle memory from coding in Ruby for too many years. I feel single expressions is the right way to go. I currently lean toward allowing guard statements as the one exception and ability to inject additional lines via Macros (for tracing etc). I’m not that opinionated about it though.

I'm ok with implicit return in a catch block, but I think there are other things that need to be figured out for Swift expressions first. Such as exactly what exceptions will be allowed to the single-line block rule. Allowing "guard" before the expression seemed to be a popular direction when if/switch expressions were debated.

I'd be down with something like this I think. Allowing a do block to contain any extra side-effect code. I'd rather see it in another context first. I don't think this is the right time to pitch expressions in exception handling.

let result = try fetchString() catch { 
  // do block is allowed here to inject a side-effect
  do { log.error(error.localizedDescription) } 
  nil
}