-1

Swift's guard statement is awesome for early exits. In some scenarios, we might want to perform one call besides exiting using return.

final class AppCoordinator {
    func showApplePaySplash() -> Void { /* some presentation logic */ }
}

final class OnboardingCoordinator {
    init(settings: Settings, parent: AppCoordinator) {
        // This code should probably use a `switch` statement and not `guard`, but I am curious about this
        guard settings.hasSeenApplePaySplash else { 
            parent.showApplePaySplash() // method returning `Void`
            return
        }
        // Some more logic...
    }
}

What I am curious about is if it is possible to shorten the syntax:

guard settings.hasSeenApplePaySplash else { 
    parent.showApplePaySplash()
    return
}

Since this is inside an init we cannot write:

guard settings.hasSeenApplePaySplash else { 
    return parent.showApplePaySplash() // compilation error: `'nil' is the only return value permitted in an initializer`
}

We can change the four lines to this oneliner of course:

guard settings.hasSeenApplePaySplash else { parent.showApplePaySplash(); return }

Which reads out quite nicely IMHO. But I still would like to get rid of that return (because I am curious if it is possible. No need to tell me: "just use return man").

In this other scenario, where we would like to guard against some undefined bad behavior/state:

guard index < myArray.count else { fatalError("Array out of bounds exception, did you think about X, Y, Z?") }

We do not need to write return, since the method fatalError returns the specific type called Never.

Note: code below this point is just experimental driven by curiosity since it is bad Swift code:

So if we could change the signature of:

func showApplePaySplash() -> Void

to use Never, like so:

func showApplePaySplash() -> Never

Then we could replace:

guard settings.hasSeenApplePaySplash else { parent.showApplePaySplash(); return }

This is what I'm curious about, once again, not preferred or endorsed: With just:

guard settings.hasSeenApplePaySplash else { parent.showApplePaySplash() }

But Never does not have any initializer. And it seems like the only possibility of creating a Never is to use methods such as fatalError to create a crash.

I found this execellent SO answer by @guy-daher - making it possible to replace fatalError making it possible to "catch" it in tests. But it uses waitForExpectations(timeout: 0.1) which is not possible outside of test suites?

So Never is probably of no help here. Pre Swift 4 (pre Swift 3?) there was function annotation called @noreturn that seemed like it could have helped?

Is there any way of achieving this? :)

halfer
  • 19,824
  • 17
  • 99
  • 186
Sajjon
  • 8,938
  • 5
  • 60
  • 94
  • Well technically you can exit early using `guard` without `return` by using `throw`... but I don't think that's exactly what you were looking for. I'm not quite sure what it _is_ you're looking for, though, so ::shrug:: – Charles Srstka Apr 10 '18 at 15:53
  • 2
    @noreturn was the previous of `Never`. You're correct, `Never` won't help you here, because `Never` functions won't let you exit the scope. – Alexander Apr 10 '18 at 16:02
  • I see you are using the `Coordinator` pattern. Most examples I have seen use a `start()` function. Try putting your `showApplePaySplash` logic there, and only use `init` to set a reference to a VC or something like that. – koen Apr 10 '18 at 16:17
  • 1
    What you're trying to do is an explicit non-goal for Swift. It is intentional that you must explicitly return (or call something that explicitly never returns). So even if you came up with something clever that allowed it, it would be bad Swift. (Since it's just curiosity, no, it's not possible, and if it were, it would likely be removed as an unintended feature.) See "Rename guard to unless" in the list of Commonly Rejected Changes, which explains what must be in a `guard` block: https://github.com/apple/swift-evolution/blob/master/commonly_proposed.md – Rob Napier Apr 10 '18 at 21:07
  • @Alexander When you say "`Never` functions won't let you exit the scope," I'm not sure what you mean. `Never`-returning functions *always* exist the scope, because the program must terminate before they return (typically by crashing; but they may infinite-loop or `throw`). You can't get much more of "exit the scope" than "the whole program is gone." :D (But if they `throw`, they also exit the scope.) – Rob Napier Apr 10 '18 at 21:09
  • @RobNapier What I meant was they can't let you exit just this particular scope (the one within which the guard statement is), it's an all-or nothing kind of deal. `throw` works, (whether from a `Never` function or not), but my main point is that `Never` wouldn't be the mechanism by which OP would achieve what he wants. – Alexander Apr 10 '18 at 23:54
  • I know it would be bad Swift, I was really just curious if it was possible! Feels like I am being punished with downvotes and slander for being curious... So sounds like it is not possible. – Sajjon Apr 11 '18 at 07:37
  • 1
    @koen it was just some example code. I am using RxFlow for coordinator pattern and this is actually a "Stepper" and then I got curious if `; return` could have been dropped. – Sajjon Apr 11 '18 at 07:38
  • I added disclaimers to the question to show that it is a bad idea. – Sajjon Apr 11 '18 at 07:47

2 Answers2

2

Never is the new @noreturn, and @noreturn meant that execution literally cannot ever possibly continue after the function returns. The point of Never is precisely that it is an uninhabited type and that it is impossible to create an instance of it.

Never (and @noreturn before it) have a special meaning to the compiler: when you call a function that "never returns", the compiler doesn't have to assume that there is a valid code path after the function call and can perform optimizations assuming that the code will never be executed. In practice, LLVM adds a trap instruction (like ud2 on x86) after the call to ensure that the program crashes if the function actually does return.

You could do one of the following:

  • change init? to init(...) throws, make parent.showApplePaySplash() return an Error, and use throw parent.showApplePaySplash() in the guard clause;
  • make your peace with init being special and return nil;
  • make init private, and use a class func to create your object instead (which is the best style IMHO, as my philosophy is that initializers generally should only ensure that the object is self-consistent, and that consistency with some other state should be ensured at another level).
zneak
  • 134,922
  • 42
  • 253
  • 328
  • Definitely agree that the last one is best. Just creating an object should never create visible side effects; certainly not cause UI impacts just by calling `init`. – Rob Napier Apr 10 '18 at 21:02
  • This is the pattern used by RxFlows "Stepper"s: https://github.com/RxSwiftCommunity/RxFlow/blob/develop/RxFlowDemo/RxFlowDemo/Flows/AppFlow.swift#L57-L65 but yes that should maybe be changed. I might do a PR! :) – Sajjon Apr 11 '18 at 07:48
1

Why not use defer to specify your exit cleanup code?

final class OnboardingCoordinator {
    init(settings: Settings, parent: AppCoordinator) {
        defer {
            parent.showApplePaySplash() // method returning `Void`
        }
        guard settings.hasSeenApplePaySplash else { 
            return
        }
        // Some more logic...
    }
}
Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • The whole point of the `guard` statement is to exit the scope early if conditions aren't met. Probably would be better to just use a regular `if-else` statement if there are actions to be taken afterward. – SirCJ Apr 11 '18 at 02:06
  • Agreed, but there are cases where you want your guard statement to exit a function BUT you have "cleanup" code that you want to be run before ALL returns from the function. That's what the `defer` clause is made for, and why I suggested it. – Duncan C Apr 11 '18 at 02:59
  • Because it was a simplified version... I would like to have a control flow of several steps, some enum would probably be preferred and switch case, but that was not what I was interested in. I was curious if it was possible from a language perspective. – Sajjon Apr 11 '18 at 07:34