[Pitch] `Never` as the parameter type for function that does not take a parameter

Just some thoughts after I run into this problem.

It seems a common practice that nowadays we need to write a dedicated overloads for generic types when they are Void, just for convenient use. Take an example of class PassthroughSubject, it provides a function available when the Output type is Void, which I believe is just a simple wrapper of the other function required by protocol Subject: func send(Output). Correct me if I am wrong, but its implementation probably looks like following:

extension PassthroughSubject where Output == Void {
    func send() {
        self.send(())
    }
}

The problem here is, taking a parameter of type Void is not the same as taking no parameter.

But what if we can make all functions take a parameter? Not explicitly, at least.

And we have the tool already in the standard library: the Never type.

Several benefits I can think of:

  • It expands the coverage of the generics. With this, the generics not only cover the scenario when there is a type presented, but also when there is none. No more overloads just for convenience, the function() is implicitly function(Never). So we don't need to extend the generic types to have func send() for Subject<Void, ...>, because that is already available as Subject<Never, ...>

  • enum types can also take advantage of this, a case without associated values is implicitly a case with an associated value of type Never. Take Result<Success, Failure> as an example, .success is automatically available because that is the success case for Result<Never, ...>. And the cool thing about it is: the complier will forbid you from using the parentheses because there is no way construct an instance of Never type.

  • It co-exists with function that takes a parameter of Void take. To be honest, these function overloads for Void type gives people a false impression that having a parameter of Void type is as good as having no parameter at all. But with this change, taking no parameter is no longer a sub-category of the Void type, but at the same level with Void type. Because, taking no parameter should never be a special case for just the Void type.

I’m confused by what you’re suggesting. You can never create an instance of Never. That’s its whole purpose. So a function with an argument of type Never cannot be called; a function with a return type of Never can never return.

8 Likes

Yes, this is to take advantage of that feature. The way to call a function func bar(_ arg: Never) is bar(), because you cannot create an instance of type Never. So I am suggesting to make a function that takes no parameter an implicit function that takes a Never argument.

What does that buy us?

This is not how Never and Void work. Never is the uninhabited type, meaning that it has zero possible values. Void is the unit type, meaning it has exactly one possible value. When you use Never, you’re explicitly telling the compiler that this thing is impossible to create. When you use Void you’re telling the compiler that all instances of this thing have the same trivial value ().

To illustrate this, let’s assume that Swift required you to have an associated value for each case in an enum. How would we define Optional then? We could try to use Never as the associated value for the none case:

enum Optional<Wrapped> {
  case some(Wrapped)
  case none(Never)
}

But that wouldn’t work, because now we can’t create any .none() values. There’s no value to put between the parentheses, because there are no values of type Never. In fact, the compiler would be allowed to assume that any value of our Optional type is always .some(Wrapped), because we’ve explicitly told it that it’s impossible to create a .none() value. That’s definitely not what we want.

Let’s try with Void instead:

enum Optional<Wrapped> {
  case some(Wrapped)
  case none(Void)
}

This would work. To create a .none() value, we just write .none(()). A bit unweildy, but it works. And when pattern matching, we can skip the value entirely and just match against .none(_) or .none, since we know that the associated value can only be ().

You can think of this as how cases without associated values already work in Swift – you just don’t have to write the (Void) or the (()).

19 Likes

I haven't thought carefully enough about this to be agreeing or disagreeing, but I think what this could buy us is, for example, that:

typealias Executable <Input> = (Input)->()

could now be used to represent ()->() by writing Executable<Never>, potentially removing the need to write a specific overload.

Of course the spelling Executable<Never> sounds like it would never be able to executed, and I'm not saying that this is a good idea, I'm just pointing out the technical "advantage" of such a change.

While I agree with everything Jonathan wrote, I think it’s worth pointing out that Never already has utility as an argument type. A simple example is Result: if you have a Result<Int, Never>, that represents a result that can only be a success, because .failure cannot be instantiated. (This is useless by itself, but can arise in generic contexts.)

A practical example of where this is used a lot is with Combine Publishers; a Publisher<T, Never> can never complete with a .failure for the same reason, and even trying to handle .failure in receive(completion:) is an error.

Generics or protocols can also produce regular functions that take type Never, and can therefore never be called, and the compiler understands this:

func blah(_: Never) -> Int {
    // No need to return anything here, because it’s unreachable
}

func blah2(_: Never) -> Int {
    // In fact, the compiler complains if we do try to return something:
    return 5 // Warning: Will never be executed
}

If we want to address the ergonomics of Void arguments in generic/protocol-witness contexts, I think it would be more feasible, as well as more conceptually sound, to allow eliding of Void arguments (or equivalently, to give them an automatic default value).

12 Likes

Given the current definition of Never, I can't imagine this would be anything but utterly confusing. It's literally giving another meaning to a type name.

I think the proposal would be better calling for an Empty keyword to represent the idea.

As others have explained, these are not benefits, but in fact actively not desired.

2 Likes

Side note: I wish Swift's Void had originally been spelled Unit, like Scala, F#, OCaml and some other (often functional) programming languages do. Swift's unit type is called Void, which seems much more like a hand-over from C and C++.

But I digress.

1 Like

Developers familiar with functional languages are a distinct minority, and those with a computer science background even more so.

I, for one, would not have known what a Unit type was without explanation, and my undergrad was in CS.

4 Likes

Sure, but Kotlin also uses Unit and people seem to learn quickly. In fact, many newer languages spell it Unit. Some languages even spell it NUL or struct{}. Python uses None. People seem to cope.

But I digress even more.

3 Likes

I think the logic still kind of fits, though. You have a function that takes no parameters. Since it is impossible to pass any parameters to it, you represent that by passing an impossible value.

Swift doesn’t model functions as maps from tuples to tuples (anymore), but if it did, functions declared func foo() -> T would map Void to T, not Never to T.

3 Likes

But "passing an impossible value" already has a definition in Swift: it means "this function is not callable".

5 Likes

It sounds like that would also give you a way to construct a value of type Never. Inside your bar function arg would contain the value that cannot be created. That sounds like it would break the concept of Never.

5 Likes

Looking at the motivating example, to avoid syntax artists feeling the need to provide beautification overloads like this:

I would say that, today, you probably just shouldn't do this at all—overloading in Swift is best avoided, because it leads to slow compile times and confusion with generics. But in the fullness of time, this is something that variadic generics would address, since you could define the original protocol as:

struct PassthroughSubject<Output...> {
  func send(_ outputs: Output...)
}

and that would let you call it as send() for no arguments, send(x) for one argument, or send(x, y) for multiple arguments, without micromanaging the overload set.

17 Likes

send() and send(_:) have different arities; are they actually overloads?

They are and they aren't, since we never fully committed to the "arity and labels are part of the name" model. It's less bad than overloading the same arity/labels, but we still have a lot of places where an unapplied reference like foo.send could resolve to either.

7 Likes

For a concrete example, this is why we don’t have count(where:) in the standard library.

1 Like