Returning Void

Does returning void make any sense? I understand why the following three examples generate the exact same compiled code, but I'm not sure if it is intended.

Example 1

func printVoid() {
    print("I'm void!")
}

Example 2

func printVoid() {
    return print("I'm void!")
}

Example 3

func printVoid() {
    print("I'm void!")
    return
}

Example 1 is pretty clear and straight forward. Example 3 has a return at its end, no problem at all. It's not necessary but ok. The compiler should probably raise a warning that it does not have any effect. (Since it is not a early return)

Example 2 is strange. I guess swift does always return void for void functions. So it is not a problem. The expression gets evaluated and then void is returned. But does that make any sense? Imho it shouldn't be allowed since it can be confusing for beginners and it does not make any difference.

Am I missing something? Is there any case where Example 2 makes sense? I'm not even sure if it is intended that example 2 is possible.

I think this is a job for the linter:
It's imho not very dangerous, and therefor not worth adding a special case in the compiler.

2 Likes

Void is a type alias for an empty tuple, (), so it does make sense? The way I understand it, every function returns a value in Swift, and there is some sugar that makes it possible to omit the uninteresting Void values.

Imho it get's really interesting with Void? ;-)
For that, afair we actually have a special case: init?-calls - they don't need return () like regular functions with return type Void? (I wish you could omit the return there as well)

I actually use version 2 a lot with asynchronous code. For example:

func someFunction(callback: () -> Void) {
  guard someCondition else {
    return callback(error)
  }
  ...
  callback(success)
}

I have a lot of guard statements like this in my server-side code. Since the code is asynchronous, I have to return information via a callback. However, callbacks alone aren't enough in a guard, so I need an additional return after them.

Before switching to this idiom, I would often forget the return, see the compiler error a second (or more) later, go back and add the return, and so on. Now I start with return, so I can't forget to add it afterwards. So for me, this does make a difference.

I also think it looks kinda nice, as it makes it clear I'm ending the function by invoking the callback.

Yes, it may be confusing to absolute beginners as, at first glance, they may think you're returning something, but that doesn't mean it shouldn't be allowed. There are way more confusing things in Swift than this. In fact, I actually include this particular example in my course as it helps teach the difference between a function and a function call, and encourages the students to think about Void.

7 Likes

The one case where Example 2 is absolutely necessary is when dealing with generics. Imagine a ‘call this block with a lock held’ function like this:

func withLock<Result>(_ body: () -> Result) -> Result {
    lock()
    defer { unlock() }
    return body()
}

You want it to work regardless of whether body returns Void or not:

withLock {
    somethingProcedural()
}
let result = withLock {
    return somethingFunction()
}

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

4 Likes

Thats true. Haven't thought of that although I've been using generic functions like that a lot.

Here is another case where you can return Void.

var errorCode = 0

func doDangerousStuff() {
  let code = /* compute */
  guard code == 0 else {
    return errorCode = code // <= here
  }
  ...
}

That is an interesting discovery, Haven't thought of that kind of use.

However, according to my humble understanding, it would be intended.

Because this feature aligns the idea of function and/or closure in Swift with function in maths, that is given a value of Set A, the function will return a value from Set B, even though there is no side effect in maths function.

In other words, every function and closure in swift would have a type A -> B, no matter what type A and B is.

Moreover, I guess this feature is required by the type system of Swift that has function type, or another name Exponential Type.