[Pitch] Conform `Never` to `Codable`

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.

I use DecodingError.dataCorrupted in the implementation, which seemed like the most relevant error.

That's true, another implementation could fatal error or throw a different specific error. I don't think that kinds of difference would pose a significant problem.

1 Like

I've posted PRs for the standard library implementation and the Swift Evolution proposal:

6 Likes