[Pitch] Conform `Never` to `Codable`

A short new proposal; implementation coming forthwith.

Introduction

Extend Never so that it conforms to the Encodable and Decodable protocols, together known as Codable.

Motivation

Swift can synthesize Codable conformance for any type that has Codable members. Generic types often participate in this synthesized conformance by constraining their generic parameters, like this Either type:

enum Either<A, B> {
    case left(A)
    case right(B)
}

extension Either: Codable where A: Codable, B: Codable {}

In this way, Either instances where both generic parameters are Codable are Codable themselves, such as an Either<Int, Double>. However, since Never isn't Codable, using Never as one of the parameters blocks the conditional conformance, even though it would be perfectly fine to encode or decode a type like Either<Int, Never>.

Proposed solution

The standard library should add Encodable and Decodable conformance to the Never type.

Detailed design

The Encodable conformance is simple — since it's impossible to have a Never instance, the encode(to:) method can simply be empty.

The Decodable protocol requires the init(from:) initializer, which clearly can't create a Never instance. The implementation throws a DecodingError if decoding is attempted.

Source compatibility

If existing code already declares Codable conformance, that code will begin to emit a warning: e.g. Conformance of 'Never' to protocol 'Encodable' was already stated in the type's module 'Swift'.

The new conformance shouldn't differ from existing conformances, since it isn't possible to construct an instance of Never.

ABI compatibility

The proposed change is additive and does not change any existing ABI.

Implications on adoption

The new conformance will have availability annotations.

Future directions

None.

Alternatives considered

None.

28 Likes

This sounds fine to me; in distribution also even if a method requires SerializationRequirement = any Codable; a Never returning method isn't "wrong" from a serialization point of view... weird yea, but as you said -- the Either (or similar) case can be useful so I'm +1 here :slight_smile:

3 Likes

This sounds pretty reasonable to me: especially if we want to have Never behave more like a bottom type, this seems like a good fit.

I do, however, wonder if this addition warrants a minor adjustment to Codable synthesis, from a practical perspective. A sum type like Either<A, B> can still be useful if either A or B are Never, since the other branch can still be instantiated and used; a product type, however, is not, if any of its fields are Never. Although this would be pretty niche, would it make sense for Codable synthesis to bail if any of the encountered fields of a product type are statically known to be Never, since the type could never successfully be instantiated?

e.g., today, you can write

struct S {
    let n: Never
}

and compile, but it's a bit difficult to write reasonable-seeming code which would appear to instantiate S successfully that the compiler would accept. If you can more easily write

struct S: Codable {
    let n: Never
}

it becomes much easier to write code which pushes this failure from compile time to runtime.

(Again, this is pretty niche, and you can already end up in this situation today yourself, but if we're making it easier to do so, maybe it's worth considering anyway?)

6 Likes

I see the concern here and perhaps refusing to synthesize an implementation in these circumstances would be reasonable. We could also emit a warning that would be silenced by providing an explicit implementation (or removing the conformance).

I’m not super concerned about this issue, though, because the only additional way to instantiate S that we’re admitting here is one which is, by contract, failable. So I’m considerably less worried about synthesizing an always-throwing implementation than I would be about synthesizing an implementation that appears to unconditionally succeed and then fatalErrors at runtime, or something.

6 Likes

I seem to recall in previously adding other conformances to Never there was a discussion as to which protocols it was desirable to include. It seems implausible that Codable wasn't considered—does anyone recall why we previously elected to omit this conformance and do any of the objections raised then still apply now?

5 Likes

From I quick look back at the pitch and review threads for SE-0215 (pitch, review) and SE-0319 (pitch, review) I was unable to find any extensive consideration of other protocols. Codable is mentioned offhand a couple times as another desirable target, and the previous Core Team guidance does not specifically request that subsequent proposals take a holistic approach and consider all possible protocols that might be useful:

I can't say with certainty whether I've missed a discussion somewhere, though.

4 Likes

Another use case: there are some interfaces that take a generic Encodable, and sometimes I want to feed it nil. I can’t use a nil literal because there’s not enough type info. I could arbitrarily say as Int? or as String? or as MyStruct?, but as Never? would be a better fit for that.

7 Likes

i do the same thing with my domain-specific encoding protocols (JSON{En, De}Codable, BSON{En, De}Codable, etc). it’s useful when interfacing with some bizarre schema that require explicit nulls and never encode anything but null.

they might seem like stupid schema (i certainly think so), but they are sadly very common. SPM especially loves encoding Never? for reasons i cannot fathom.

i'm not a fan of the standard library's Codable, i think it is too underspecialized for many of my use cases. but if Never? was useful for JSON encoding, i don't see why it wouldn't be useful for general encoding too.

3 Likes

Should it throw a DecodingError or should it fatalError()?

It should throw an error. It isn't a programming mistake to try to decode something in a generic context that might be Never.

19 Likes

This is a good pitch and it should be implemented. Thank you. :smiley_cat:

However,

  1. I don't think it's important to make Void Codable, but I do think it's nonsensical for Never to be Codable otherwise, given that you can have a () instance. (This will take more work, and shouldn't hold up the easy addition for Never.)

If Never conformed to all protocols, it would be a useful constraint to virtually remove the genericism of outer types, similar to what @bbrk24 mentions above. I don't think it's possible to find a conformance that won't be useful, and would also cause harm by being implicit. But I'm also not going to read through that whole thread and I think there's a good chance someone in there might have proved me wrong. :wink:

2 Likes

I don't understand what your takeaway here is in the end. You don't think Never should be Codable unless Void is also Codable, but you're OK with Never being Codable while Void isn't Codable, but you don't think resolving the discrepancy is important, but you don't think it's sensible not to?

2 Likes

You imply that the run-on sentence is not understandable, but it sounds great to me. :+1: The nonsense is acceptable, given the utility of the pitch and how it doesn't require tuples to adopt protocols. If they ever gain that ability, Void should be audited for where it should match Never. It's not "important" to get this right in the implementation of this pitch because doing so is non-trivial and isn't going to make a practical difference to real code.

As for part 2 of what I said, though, as many protocols should be thrown in along with Codable as possible. If the count of those is zero, it's still a win.

1 Like

agreed, moreover, i do this on purpose for schema validation. for example, here is an excerpt from some code i use to decode swift symbolgraphs:

    public
    init(json:JSON.ObjectDecoder<CodingKeys>) throws
    {
        switch try json[.type].decode(to: SymbolRelationshipType.self)
        {
        case .conformance:
            self = .conformance(.init(
                    of: try json[.source].decode(),
                    to: try json[.target].decode(),
                    where: try json[.conditions]?.decode()),
                origin: try json[.origin]?.decode())
        
        case .defaultImplementation:
            self = .defaultImplementation(.init(
                    _ : try json[.source].decode(),
                    of: try json[.target].decode()),
                origin: try json[.origin]?.decode())
            try json[.conditions]?.decode(to: Never.self)
        //  ^~~
        // should never have generic constraints in this case.
        
        case .extension:
            self = .extension(.init(
                _ : try json[.source].decode(),
                of: try json[.target].decode()))
            try json[.conditions]?.decode(to: Never.self)
            try json[.origin]?.decode(to: Never.self)
        //  ^~~
        // should never have generic constraints or source origin.

(my syntax is a bit different from what the standard library’s Codable uses, but the optionally-chained subscript is essentially how i spell decodeIfPresent.)

1 Like

I don't think it's possible to find a conformance that won't be useful, and would also cause harm by being implicit.

This has come up before: every protocol with static requirements or associated types is not one that Never can implicitly conform to, and we try to make adding requirements be a non-breaking change; thus, Never should not implicitly conform to any protocols. (I don't remember if we managed to make adding associated types be a non-breaking change as well.)

IMMEDIATE EDIT: I suppose whenever we add new requirements in a non-breaking way, we also add defaults for those requirements, and those defaults could apply to Never as well. But it applies to protocols that have those requirements already.

1 Like

:heavy_check_mark:. Summarized nicely here, I think:

Sounds good! Seems to me like this pitch should really be something like, "Conform Never to every protocol without the static/(non-throwing)initializer issue", then, but Codable's a good addition.

2 Likes

What is the reason that you argue this pitch "should" be like that?

An aside about the word “should”, for curiosity’s sake

Some time ago I put some more explicit thought into the word “should”, and I noticed some rather obvious things that nonetheless seemed extremely important to me because of how commonly it is used in everyday English, and how commonly I believe I see it contribute to conflicts.

Here’s some Swift that captures an aspect of “should” that I think is important:

protocol SentenceSubject {
    func should
        <Goal>
        (_ strategy: Action,
         inOrderToAchieve goal: Goal)
    -> PieceOfAdvice
}

if you have the subject:

struct Person: SentenceSubject { ... }

then you can create a piece of advice like this:

let you = Person()
let advice = 
    you.should(
        goBuyYourTicketFromTheTeller,
        inOrderToAchieve: yourDesiredBusTrip
    )

One of the “obvious” things I was referring to is the simple fact that “should” actually takes a second argument, namely inOrderToAchieve goal: Goal.

When I say that “should” contributes to conflicts, I’m referring to a specific, common “programming” error that I believe people make when using it. I’ll clarify:

Since the “should” function is used very commonly but is also somewhat unwieldy, whenever you’re within a domain where you can assume a particular value for the goal it’s also very common to define more ergonomic overloads of the form:

func should (_ strategy: Action) -> PieceOfAdvice

Example:
“I want to take the bus to Madrid.” (:point_left: this "macro" provides the overload should(_:))
“You should go buy your ticket from the teller in order to achieve your desired bus trip.”.

The first thing that can go wrong is that someone tries to use one of these ergonomic overloads but finds that they are actually not in a context where such an overload exists (unlike the simple example with the bus where it worked fine). I interpret @xwu ’s question as analogous to a compile-time error telling @anon9791410 that in his (Xiaodi’s) current context there is no such overload should(_:).

This is actually not any kind of issue in my mind though. The listener emits a compile-time error and it gets worked out promptly.

There’s a worse (but also more common) outcome, which I think derives from a strange cultural aversion that we seem to have to this type of “compile-time error” in conversation (i.e., slowing down to clarify before responding). What I think I see happen often is that (because of the aversion to compile-time errors) the listener accepts the usage of the overload despite not having a sound way to resolve it to a particular meaning in the current context, and so tries to use some combination of unsound methods to come up with the best meaning they can and then uses that to respond to the statement (probably erroneously). This is analogous to a “runtime bug”. If both participants suffer from the aversion to using “compile-time errors” in conversation then it is likely that these misunderstandings will stack up, as each person responds to the flawed response with another response that is born of faulty understanding mixed with a strange insistence on barreling ever-forward.

(Analogy: Imagine SwiftGPT, a language model that accepts absolutely any “Swift” code that you write and compiles the “closest” correct program that it can think of to what you’ve written. If what you write compiles then it’s guaranteed to come out identical, but if you have any compile-time error then instead of being told about it you just give SwiftGPT license to change whatever it “needs” to in order to make it “work”. What a nightmare! We’d never ship another stable thing! The point is, what a blessing compile-time errors are.)

(After writing that passage about SwiftGPT I realized that maybe it could be made well enough to actually work fine, and that maybe that’ll be more or less the future of programming… :thinking::man_shrugging:)

Concretely, I think that in the realm of politics in particular there is a lot of this kind of “should” thrown around without much precision about the second parameter (inOrderToAchieve:), and that a lot arguments will go in circles until someone thinks to clarify what each person’s goal is when they imprecisely say “we (as a country) should {xyz}”. In the case where the people have different goals in mind, the argument won’t be immediately resolved, but it will at least be clarified that the disagreement is actually about what the goals are, and not about the effectiveness of a particular strategy toward a particular goal, which can help the participants move forward. If/when they eventually converge on a shared goal, then they can finally debate the effectiveness of different strategies toward that goal.

Thanks for reading!

4 Likes

+1. Sounds like a sensible addition.


Which specific error? DecodingError.typeMismatch?

Couldn't the new conformance for Decodable's init(from:) requirement differ from an existing conformance? (Not that I think it matters — conforming a type you don't own to a protocol you don't own is already a bad idea.)

off-topic response

This reminds of the is-ought problem.