Why does Swift allows to compile code with Never as an input argument?

My team recently faced an issue where Combine Publisher was parameterized with Never output, and still Swift was able to map etc over the value even though such constructs would never execute.

Would it be better if Swift will error on a code as below, as code in the map would never execute and probably does not match an expectation of an engineer.

import Combine
Empty<Never, Never>().map { _ in 
    42
}

Would like to hear what I am missing here, and what sort of a value can overweight the potential issues of such code to compile. Thanks.

Functions taking a Never parameter compile, but produce a warning:

public func foo(_: Never) {
  print("Hello world")
}
$ swiftc q.swift
q.swift:2:3: warning: will never be executed
  print("Hello world")
  ^
q.swift:1:27: note: '_' is uninhabited, so this function body can never be executed
public func foo(_: Never) {
                          ^

I guess we don't do this for closure parameters for some reason, but we probably should. Do you mind filing a bug at bugs.swift.org?

2 Likes

Thanks! I actually reported this in FB7684035 towards Combine.
Do you think the code like following will be also covered with warning?

public func foo(_: Never) {
  print("Hello world")
}

public func bar(_: @escaping (Never) -> Void) {
}

bar(foo) // Will this generate a warning?

@escaping (Never) -> Void is not an uninhabited type, since the closure { (_: Never) in } is a valid witness. Also, since bar() has an empty body, the warning would not be emitted even if the argument type were uninhabited.

2 Likes

I see, thanks for clarification. So there is a possibility still to sneak the bug into the code even after all the warnings actually fixed. I guess this code below would never execute but will compile normally without warnings.

public func foo<T>(_ input: T) {
  print("\(input)")
}
[Never]().map(foo)

This is actually a reduced case of something we had in production.

Well, this really depends on what the type is.

enum X<T> {
  case a
  case b(Int)
  case c(T)
}

func f<T>(_ input: X<T>) {
  print("\(input)")
}

let x : X<Never> = .b(3)
f(x)

You can imagine more complex reasoning in the presence of nested types.

Even with the array example, it requires some non-trivial computation to be done in the general case, as the compiler can't know what a human can know by "looking" at the code. After all, you could perfectly well have an empty array of type [Never].

The code you provided is good, and absolutely legit. I do not see reasons why it shouldn't compile, or why it should emit a warning.
Meanwhile in example provided before, map that takes Never as an input type would never be possible to execute. Having an empty array of Never as shown in my example does not make the map over array legit. Developers should be informed about such issues in their code.

I kinda expect some smartness from compiler, where it may notice a dead branches of code and make developer aware about their existence. It seems this is an expectation from Swift as such warning is already exists, however there are cases that are impossible but still not covered with warnings.

How much compiler engineering time would it take to design the logic, and would running the logic slow down compile times for all code?

I don't think you got it right here. An empty array of Never (in code, [] as [Never] or [Never]()) is just as valid a value as a nil of type Never?, or as valid as an empty array of anything, isn't it? From nothing comes out nothing.

In the same regard, you can freely map with any transform function these things into other types, and you've still got zero instances of something (i.e. nothing) in your hand.

6 Likes

Mapping an empty array does something though - it produces a new, empty array of a different element type.

It’s a bit of an awkward way to make an empty array, sure, but the compiler doesn’t warn you about awkward code.

1 Like

[Never]() is the same as [String]/[T](), it's empty array no matter whether the element is Never or not, so it map nothing.

I feel like the empty array is a bit of a red herring here. Regardless of whether the compiler knows that the array is empty, the map will provably never execute. Consider:

func foo1(_ never: Never) -> Int {
    return 0 // warning!
}

func foo2<T>(_ t: T) -> Int {
    return 0
}

func bar(arr: [Never]) {
    print(arr.map(foo1))
    print(arr.map(foo2))
}

We get a warning at the declaration of foo1 that its body will never execute. So I believe the question @Nikita_Leonov is asking is, would it be appropriate to have a warning at the specialization site of foo2 that it will never be executed? It would need to be able to be silenced, since it is at least sometimes a legitimate operation (e.g. with an AnyPublisher<Success, Never> that you're forced to adapt to an AnyPublisher<Success, SomeError>), but the use of a generic function that gets specialized to Never may be an indication that an expectation of the programmer has been violated. You could silence using as, e.g., arr.map(foo2 as (Never) -> Int).

2 Likes

Thanks, everyone. Indeed after reading @Karl's message, I realized there is a value in having a code with Never to compile. We actually do use it every time in scenarios like Combine’s setFailureType function that allows converting Never to some Error. So operating on input values of type Never should be allowed, but probably discouraged by emitting a warning in all the cases except suppressed ones.
@Jumhyn properly noticed that my example was focusing not on an empty array but on the map from (Never) -> Void. Such constructs except for a couple of edge cases could be a source of hard to find and easy to introduce errors. I really like an idea of emitting warning in all the cases, but meanwhile provide a way to suppress it in rare cases where it is actually needed and expected. @Jumhyn did suggest an elegant solution, where a developer in such a rare case needs just to be explicit in intent, this way it will be obvious that this is not a mistake but the desired implementation. As a result developer will be able to change types, without worry to miss issues like above.

1 Like

@Nikita_Leonov At this point, I would recommend filing the closure issue and the generic specialization issue at bugs.swift.org, and/or posting over at #evolution:discuss to see what the broader community thinks about emitting a warning in these cases, and whether such a change would require an Evolution proposal, or if it would be considered a bug fix.

1 Like