[swift-evolution] [Review] SE 0192 - Non-Exhaustive Enums

Xiaodi Wu xiaodi.wu at gmail.com
Tue Jan 2 14:09:55 CST 2018


On Tue, Jan 2, 2018 at 1:46 PM, Matthew Johnson <matthew at anandabits.com>
wrote:

>
>
> Sent from my iPad
>
> On Jan 2, 2018, at 12:48 PM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
>
> On Tue, Jan 2, 2018 at 9:38 AM, Matthew Johnson via swift-evolution <
> swift-evolution at swift.org> wrote:
>
>>
>>
>> Sent from my iPad
>>
>> On Jan 1, 2018, at 11:47 PM, Chris Lattner <clattner at nondot.org> wrote:
>>
>> On Dec 31, 2017, at 12:14 PM, Matthew Johnson via swift-evolution <
>> swift-evolution at swift.org> wrote:
>>
>> I agree that we need a solution to the problem described.  I also agree
>> that non-exhaustive is most in keeping with the overall design of Swift at
>> module boundaries.  However, I believe this proposal should be modified
>> before being accepted
>>
>>
>> Thanks for writing this up - you’ve explained a common concern in an
>> interesting way:
>>
>> This is likely to be a relatively rare need mostly encountered by 3rd
>> party libraries but it will happen.  When it does happen it would be really
>> unfortunate to be forced to use a `default` clause rather than something
>> like a `future` clause which will produce an error when compiled against an
>> SDK where the enum includes cases that are not covered.  I can imagine
>> cases where this catch-all case would need to do something *other than *abort
>> the program so I do not like the `switch!` suggestion that has been
>> discussed.  The programmer should still be responsible for determining the
>> behavior of unknown cases.
>>
>> ..
>>
>> While library authors have a legitimate need to reserve the right to
>> introduce new cases for some enums this need can be met without taking away
>> a useful tool for generating static compiler errors when code does not
>> align with intent (in this case, the intent being to cover all known
>> cases).  Switch statements working with these kinds of enums should be
>> required to cover unknown cases but should be able to do so while still
>> being statically checked with regards to known cases.
>>
>>
>> I think that this could be the crux of some major confusion, the root of
>> which is the difference between source packages and binary packages that
>> are updated outside your control (e.g. the OS, or a dynamic library that is
>> updated independently of your app like a 3rd party plugin).  Consider:
>>
>> 1) When dealing with independently updated binary packages, your code
>> *has* to implement some behavior for unexpected cases if the enum is
>> non-exhaustive.  It isn’t acceptable to not handle that case, and it isn’t
>> acceptable to abort because then your app will start crashing when a new OS
>> comes out. You have to build some sort of fallback into your app.
>>
>> 2) When dealing with a source package that contributes to your app (e.g.
>> through SwiftPM), *YOU* control when you update that package, and therefore
>> it is entirely reasonable to exhaustively handle enums even if that package
>> owner didn’t “intend” for them to be exhaustive.  When *you* chose to
>> update the package, you get the “unhandled case” error, and you have
>> maximal “knowability” about the package’s behavior.
>>
>>
>> It seems that your concern stems from the fact that the feature as
>> proposed is aligned around module boundaries, and therefore overly punishes
>> source packages like #2.  I hope you agree that in case #1, that the
>> feature as proposed is the right and only thing we can do: you really do
>> have to handle unknown future cases somehow.
>>
>> If I’m getting this right, then maybe there is a variant of the proposal
>> that ties the error/warning behavior to whether or not a module is a source
>> module vs a binary module.  The problem with that right now is that we have
>> no infrastructure in the language to know this…
>>
>>
>> Hi Chris, thanks for your reply.
>>
>> The concern you describe isn’t exactly what I was describing but it is
>> related.  John McCall recently posted a sketch of a solution to the concern
>> you describe which looked great to me.  I don’t have time to look up the
>> link this morning but I think it was in this review thread.
>>
>> The actual concern I am describing is where a 3rd party library (or app)
>> wants to switch over a non-exhaustive enum provided by a module that is a
>> binary (not source) dependency.  The author of the 3rd party library may
>> have a legitimate reason to switch over an enum despite the author of the
>> binary module reserving the right to add additional cases.
>>
>> When this circumstance arises they will do it using the tools provided by
>> the language.  Regardless of the final language solution they obviously
>> need to cover unknown cases - their library could be shipping on a device
>> which receives an update to the binary dependency that contains a new
>> case.  I agree with you that a language-defined crash is not appropriate.
>> The author of the switch must take responsibility for the behavior of
>> unknown cases.
>>
>> I am arguing that these “pseudo-exhaustive” switch statements *will*
>> exist in the wild.  The crucial point of contention is whether or not the
>> language provides assistance to the author of the 3rd party library in
>> updating their library when the enum provided by the binary dependency
>> changes.  Is the author forced to use a `default` case which turns of
>> exhaustiveness checking?  Or are they able to use an alternative mechanism
>> for handling unknown cases which does not turn off exhaustiveness checking
>> - all *statically known* cases must be covered.  The most common example
>> of such a mechanism is the `future` (or perhaps `unknown`) case which would
>> only be used for cases that *are not* statically known.
>>
>> This facility will of course help authors of these switch statements make
>> the necessary updates as the enum vended by the binary dependency changes.
>> It *will also *help alert authors of apps that depend on that 3rd party
>> library (which will usually be a source dependency).  If the author of the
>> app attempts to rebuild the dependency against a new SDK with added cases
>> the library will fail to build, alerting the user that they should update
>> the 3rd party library.
>>
>> My position is that if there are *reasonable* use cases for these kinds
>> of “pseudo-exhaustive” switches then the language should provide
>> exhaustiveness checking of statically known cases via some mechanism that
>> authors can opt-in to using.  It’s ok with me if this is a relatively
>> esoteric feature.  It won’t be *commonly* needed, but when it is
>> necessary it will provide significant value.
>>
>> IIRC there were some reasonable examples of these kinds of switches
>> posted in earlier threads on this topic.  I don’t have time to look those
>> up right now either but it would be good for the core team to be aware of
>> them before making a final decision.
>>
>> I am only aware of two arguments *against *this kind of
>> “pseudo-exhaustive” switch.  One is that users should not attempt to switch
>> over an enum that a library author does not intend to be exhaustive (i.e.
>> it is an “input-only” enum).  The other is that a `future` or `unknown`
>> case is not testable.
>>
>> The first argument is a moral one which I believe should not carry much
>> weight relative to concrete, pragmatic counter-examples such as those that
>> (IIRC) were provided on this list in the past.
>>
>
> I think the discussion is converging on the defect here, but I disagree
> that the first argument is merely a "moral one"; it is an epistemological
> one.
>
> The epistemological dilemma is: how can the user of the library "know" all
> the cases when the library "author" has made it clear that even _they_
> don't know all the cases? Obviously, the library user can only switch over
> the cases that _he or she_ knows about. And if the user has forgotten about
> some cases that are documented, then clearly, he or she doesn't know about
> it.
>
>
> The purpose of the requested modification is to ensure that the user’s
> knowledge of cases matches the compiler’s knowledge of cases.
>
> I disagree with your framing of the argument.  I don’t mean to split
> hairs, but I view it as a moral argument with an epistemological basis.
> The argument says that because of the epistemological uncertainty
> introduced by the possibility of unknown future cases the language should
> not assist a developer who attempts to cover all cases that are knowable by
> the compiler at compilation time.  It is a judgement that such a language
> feature is undesirable for one reason or another.  This is fundamentally a
> value judgement.
>

I would disagree with you here. It's not a value judgment to say that,
semantically, there is no meaningful distinction between cases the compiler
knows about that you don't know about, and future cases neither you nor the
compiler knows about.

The argument here is rather that it should be possible to ensure that the
> user has switched over all the cases that _the compiler_ knows about.
> Suppose we accept the argument that we want to enable the user to deal with
> the largest possible set of known cases: why does it have to the compiler
> that does the warning? why is this not a task for a linter if so desired?
>
>
> Only the compiler is able to do this with 100% accuracy.   Even if we
> assume that a linter is able to correctly validate this condition, the
> source will not have the necessary information.  How should a linter
> distinguish intentional use of `default` from one which is *only* intended
> to be used when a value that is not known at compile time is encountered?
> This will require a pseudo-linguistic keyword comment such as `/* unknown
> */ default`.  I hope we can agree that encouraging linters to introduce
> language variations like this is undesirable.
>

There are many ways to skin the cat, as it were. I do not agree that
comments for linters is undesirable, for one. But seeing as we clearly have
a difference of opinion here, this reinforces the idea that this is a great
task for a linter. *My* linter of choice could read the comments. *Yours*
could, for example, replace every `default` which doesn't cover cases known
to the linter with

```
case knownToLinter1, knownToLinter2, knownToLinter3:
  fallthrough
default:
  // ...
```

in what other circumstances do we insist that the compiler inform the end
> user about future additions to the API at compile time?
>
>
> This isn’t a request for the compiler to inform the user about future
> additions to an API.  It is a request to validate the compiler’s knowledge
> of the *current* state of an API with the *current* state of the source
> code.
>

Well, it's of course impossible to inform the user about future additions,
so that's poorly phrased on my part. It's about the compiler informing the
end user about *new* additions, part of the *current* state of the API,
that have cropped up since the user last revised the code when the API was
in a *previous* state (or, indistinguishably, members of which a user is
unaware regardless of the temporal sequence of when such members were
added). In what other circumstances do we insist that the compiler perform
this service?


> The second argument doesn’t make sense to me: as I noted in my review
>> post, the code path is equally untestable when a `default` case is used,
>> but all *statically known* cases are matched in earlier patterns.  The
>> testability problem of non-exhaustive enums is orthogonal to the issue of
>> language support for “pseudo-exhaustive” switches.
>>
>> This is the line of reasoning which leads me to conclude that we should
>> dig up the concrete examples which have been provided and evaluate them for
>> merit.  If we can’t discard them as a bad coding practice for which a
>> better solution is available then we should strongly consider providing
>> language support for these use cases.  The existence of such use cases
>> should also motivate a solution to the testability problem.
>>
>> - Matthew
>>
>>
>> -Chris
>>
>>
>>
>>
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20180102/eaf76c77/attachment.html>


More information about the swift-evolution mailing list