Adding Either type to the Standard Library

To me, the utility comes mostly from the nested Error type.

/// Given two options, choose only one.
public enum OneOfTwo<Option0, Option1> {
  case option0(Option0), option1(Option1)
}

public extension OneOfTwo {
  enum Error: Swift.Error {
    case both(Option0, Option1)
    case neither
  }

  /// - Throws: OneOfTwo<Option0, Option1>.Error
  init(_ option0: Option0?, _ option1: Option1?) throws {
    switch (option0, option1) {
    case let (option0?, option1?):
      throw Error.both(option0, option1)
    case (nil, nil):
      throw Error.neither
    case (let option0?, _):
      self = .option0(option0)
    case (_, let option1?):
      self = .option1(option1)
    }
  }
}
public extension Result {
  /// - Throws: OneOfTwo<Success, Failure>.Error
  init(success: Success?, failure: Failure?) throws {
    switch try OneOfTwo(success, failure) {
    case .option0(let success):
      self = .success(success)
    case .option1(let failure):
      self = .failure(failure)
    }
  }
}

But I think that that half of the argument, in this specific case, is actually very important. If someone doesn’t agree that for this case the duality of constructions results in equal privilege, we should probably discuss this specific point (further, given that I already spent a few words on it).

I very much agree with @Nevin that some concrete examples would really help push this discussion forwards. @anon9791410 could give an example where you'd want to use a nested Error type?

1 Like

Apologies if I wasn't clear. I agree (and think that most do) that dual construction == equal privilege, what I think not every agree is that duality == should have equal privilege.

A few examples from top of my head.

  • Combined error types without a new explicit error type for every new combination of errors which would contain 2 or more errors.

  • SwiftUI‘s function builder would benefit from variadic generics and a generalized version of buildEither which would accept more than just two views. This would also beneficial for switch expressions in function builders, which do not necessarily return the same type.

3 Likes

What follows is a bunch of examples that show how anonymous sum types can be not only useful in Swift, but could also push the language ahead in the competition space of current general purpose programming languages, and attract potential users that want a more powerful type system, without renouncing simplicity and accessibility.

A possible design

To show the examples, I need to come up with a preliminary design and syntax for the feature. First of all, with "anonymous sum types" I mean "non nominal" types similar to tuples, with same (but dual) features, and the same semantics of enums. These types could be defined as follows:

  • (A | B | C) means "either A or B or C", and each case can be accessed with the patterns case .0(let a), case .1(let b) and case .2(let c), and constructed in a similar way, with .0(a), .1(b) and .2(c);
  • optionally, each case can have a label, like (first: A | B), that would be accessed via case .first(let a) and case .1(let b), and constructed via .first(a) and .1(b);
  • if a case has no associated type, it could be written as just the label, without the type, like (some: A | none:), would be accessed with case .some(let a) and case .none, and constructed via .some(a) and .none;

This is just a possibile syntax, to show the examples: the specifics should be discussed, but the base features are there. Also, let's invent some name: let's call them union tuples.

Notice that we could achieve some of the results highlighted in this post with a weaker addition to Swift, that is, a bunch of Either<A, B, ...> types in the standard library, but it would be a lot less useful, for not being able to define case labels on the fly, and for the awkwardness in handling cases with no associated types.

How to come up with examples? It's simple. Just consider every case where a regular tuple is used, or could be used, replace it with a union tuple, and examine what you got there. This forms the basis of the "dual construction procedure" where I take an available construction, produce (or more appropriately, "discover") the dual one, and examine the result. I'm pointing this out because many people are probably not familiar with what duality means when dealing with abstract mathematical concepts (for example, type systems). In very simple terms, "dual", for a concept or structure, means "translated into a similar concept or structure where relationships are reversed". The dual transformation doesn't mean anything in itself, nor proves anything other than the fact that something "exists": it just allows to "discover" stuff. But, in some cases, what we discover turns out to be extremely useful.

A prime example is the structural duality between structs and enums: both are extremely useful when modeling domains, because they allow for a correct representation of entities for which two or more things could be grouped together (structs) or alternative and mutually exclusive states (enums) could be defined. For a better understanding of the reason why structs and enums are dual to each other, I suggest to read the "Bonus Theory" section of this old post of mine.

Union tuples would be the structural dual of regular tuples, so we can try and consider examples of tuple usage, replace with union tuple, and examine what we end up with.

Heterogeneous sequences

The zip function, in Swift, takes two sequences, respectively of A and B, and produces a single sequence of (A, B). A sequence of tuples is a sequence in which each element consists of two distinct things: if we had no first-class tuples in Swift, the zip function would probably produce a sequence of some Tuple<A, B> struct, that would be much more awkward to work with.

What's the structural dual of a sequence of (A, B)? It's a sequence of (A | B), of course. And what does it mean? Well, each element of this sequence is either A or B: this is the exact definition of an heterogeneous sequence, that is, a sequence where elements could be different from each other. I'm sure many of us needed at least once to represent this kind of sequence: there are "dynamic" ways of doing it, of course, like defining a sequence of Any, or putting all possible types under an umbrella protocol, but we like to be precise, and take advantage of the type system (not to mention, dynamic solutions are slow).

Consider for example a Swift library for defining forms. There could be different types of fields, like TextField, RadioButtonField and SubsectionField. If we wanted to define a collection of fields, in order to work on all our fields together (for extracting some information, for example), a precise way to do this could be to define a specific FieldKind enum, like the following:

enum FieldKind {
  case text(TextField)
  case radioButton(RadioButtonField)
  case subsection(SubsectionField)
}

Then, we would work with some [FieldKind] array. But the only reason we defined this enum is to be able to work with such heterogenous collection. Also, if we had the specific collections of single fields, to "merge" them into an heterogeneous collection, we would need to tediously do the work by hand, without the option to generically define an algorithm to merge collections of A, B or C into a single collection of (A | B | C). With union tuples, we could generically define a function like the following:

func merge<S1, S2>(_ s1: S1, _ s2: S2) -> Merge2Sequence<S1, S2> where S1: Sequence, S2: Sequence { ... }

/// Merge2Sequence<S1, S2>.Element == (S1.Element | S2.Element)

Then, from a Sequence of (A | B), we could define generic methods like:

extension Sequence {
func partition() -> ([A], [B]) { ... } where Element == (A | B)

func collapse() -> ([(A, B)], discarded: [(A | B)]) { ... }  where Element == (A | B)

Heterogeneous streams

Many things that we can say for sequences, we could also say for streams (observables). In the various FRP frameworks we sometimes deal with streams of tuples, like Observable<(A, B)>: that's the case when we want to move more than 1 thing at a time further into the signal chain (for example, some kind of context). What's an Observable<(A | B)>? It's a signal where each event can be of either one of 2 types. This naturally occurs many times when using FRP, when an observable chain "splits" in 2. For example, given a user's behavior, we could produce Cake or Broccoli:

let food = Observable<User>.getFromServer()
  .map { /// Swift should be able to infer the type here
    $0.didBehave 
      ? .0(Cake())
      : .1(Broccoli())
  }

What you would normally do, here, is to produce 2 distinct observables, handle them separately, then merge them: the problem here, other than being forced to juggle many observables and having to instantiate more stuff, could be one of sharing the subscription between the 2 observables, in order to avoid duplication of emissions.

With an Observable<(Cake | Broccoli)>, we could handle all in a single chain:

let ingredients = food
  .flatMap0 { cake in
    getIngredientsFromServer(for: cake)
  }
  .map1 { broccoli in 
    [Ingredient(broccoli)]
  }
  .merge() /// Observable<[Ingredient]>

In general, when there's some kind of sequence, stream, stack, dictionary of heterogeneous things, if we want to model it precisely, we need to use enums: the problem is, there's no reason to define specific, nominal types for this use case, and the possibility to define anonymous enums on the fly, and to talk generically about them (like an enum of A or B) unlocks great conveniency and powerful algorithms.

Functions: alternative return types

Tuples allow for one of the most requested programming language features of all time: returning "more than one thing" from a function. A function like the following

extension Collection {
  var destructured: (head: Element, tail: SubSequence)? {
    first.map {
      (head: $0, tail: dropFirst())
    }
  }
}

returns (optionally) 2 things: the product of those things doesn't make a "thing" in itself (hence, it doesn't need a named type, like a struct), and it's more than enough to use a purely structural type like a tuple.

What's with a function returning a union tuple? We actually already have something very similar in functions that return the Result type:

extension String: Error {}

extension Dictionary where Value == Any {
  func get<A>(_ type: A.Type, at key: Key) -> Result<A, String> {
    guard let value = self[key] else {
      return .failure("No value at \(key)")
    }

    guard let typedValue = value as? A else {
      return .failure("Value at \(key) is not of proper type: required \(A.self); found \(value)")
    }

    return .success(typedValue)
  }
}

But Result is only for a very specific subset of all cases where we might want to return a sum type: in particular, it's for "failable" functions, that either return a value, or an Error. With Result, the secondary type must be an Error, and there is no third option.

A common case where we might want to have a third option is for computations that can fail but that can also be canceled. Instead of adding another specific named type in our toolbox, we could simply use a union tuple:

func executeFailableCancelableOperation() -> (succeeded: Value | failed: Error | canceled:) { ... }

Notice that the canceled: case has no associated type. One could express a similar idea with an optional Result<Value, Error>?, but the nil case wouldn't be clear. In fact, another important feature of union tuples is exactly the added clarity. Consider the differences between the following definitions:

func executeFailableCancelableOperation() -> Result<Value, Error>? { ... }

func executeFailableCancelableOperation() -> (completed: Result<Value, Error> | canceled:) { ... }

In general, a union tuple can be used to clearly signal alternative states even when there's no associated type, like for replacing a Bool where more informative labels can add the appropriate context:

/// This a classic code smell, where we're returning a value, from a function, that describes something about what the function did, but with the inappropriate type, even if there's only 2 options: the execution started, or it didn't.
func startExecution() -> Bool { ... }

/// With an union tuple, we can use labels to represent the alternative states, and the function signature becomes much more clear.
func startExection() -> (started: | cannotStart:) { ... }

I actually still didn't consider the prime example for using union tuples in function return types: when we're simply returning either a certain thing, or another, without any of them being the "main" one.

var foodRequirement: (
  Milk |
  Cookies
) { ... }

Even better, the two tuple types could be combined for defining more sophisticated structures:

var foodRequirement: (
  Milk |
  Cookies |
  (Milk, Cookies)
) { ... }

That's it: we can easily discover that Result is a very special case of a more general thing that union tuples model very well.

Functions: alternative inputs

What about function inputs/arguments? Do we use tuples in function arguments? Not really, and that's because functions can be defined as accepting more than one argument. A function accepting 2 arguments A and B is essentially the same as a function accepting a single argument of type (A, B); I wrote essentially the same because they're not completely the same: function arguments can be @escaping or not, can have default values, can be inout or @autoclosure, so there are differences. But if we esclude these cases we can say that, in essence, func foo(_ a: A, _ b: B) and func foo(_ ab: (A, B)) have basically the same semantics. So, dually, what does it mean func foo(_ ab: (A | B))? Well, it's a function that can be called with A or B: not both of them together, but either one works fine.

A function that can be called with 2 different arguments is, essentially, a pair of overloaded functions:

func foo(_ a: A) { ... }

func foo(_ b: B) { ... }

/// The pair is equivalent to:

func func(_ ab: (A | B))

We could introduce argument labels to avoid overloading, but if the base name is the same (and argument labels don't qualify the name any further) it should mean that's we're probably dealing with the same operation, just applied to different things:

func foo(a: A) { ... }

func foo(b: B) { ... }

/// The pair is equivalent to:

func func(_ ab: (a: A | b: B))

Is there a particular advantage in defining a function with a union tuple instead of 2 or more overloaded/base-homonym functions? I can think of a few:

  • overloaded/base-homonym functions can be confusing to use and understand, because to actually be aware that the same function could be called with different arguments I need to read all definitions (for example in autocompletion), so the lone signature in itself is not enough;
  • in cases like these, it's frequent to have some code that's shared among the functions, and that requires a definition of an additional private function for the common part, that's called from all the specific functions;
  • if I'm writing a library, each new public function of this kind extends the library surface.

Consider for example a function to handle and process some kind of "event":

struct Action {}
struct Change {}

enum Event {
  case action(Action)
  case change(Change)
}

func handle(_ event: Event) { ... }

The enum Event is really there just for defining a single function to handle everything. It has no methods, and cases' names are the same as the associated types, so there's no special semantics attached to the type, but without it, we would need to define two separate functions:

func handle(action: Action) { ... }
func handle(change: Change) { ... }

which could make us incur in the problems I listed earlier. With union tuples, we could have both simplicity and expressive power:

func handle(
  _ event: (
    action: Action |
    change: Change
  )
) { ... }

Also, if we needed to add a SideEffect event, it would be as easy as modifying the function signature (in a backwards-compatible way):

func handle(
  _ event: (
    action: Action |
    change: Change |
    sideEffect: SideEffect
  )
) { ... }

Interestingly, if this was a public function in a library, we wouldn't need to expose a public Event enum, to which adding a case wouldn't be, semantically, backwards compatible! Even if it wouldn't be breaking (if no @frozen), in cases where one cares about what they're publicly exposing, defining a public enum for mere structural purposes exposes a nominal type to the users that really shouldn't be.

It can be also really useful to use union tuples without associated types as function parameters dedicated to select some kind of option, like for example:

func compute(_ strategy: (statically: | dynamically:)) -> Int { ... }

let x = compute(.statically)

Also, adding cases would be simple and expressive.

Conclusion

Everyone loves examples, and they're certainly useful to explain something. But I think that the case for union tuples can really stand on its own just from the basic theory, thanks to dual relationship with regular tuples: this very relationship is the actual argument here. Examples are not arguments, in deductive reasoning. Examples are arguments only in inductive proofs, something that's basically impossible in software development, because there's potentially an infinite number of cases, and an huge number of ways in which any possible thing could be done. I'm sure that many people don't care about anonymous sum types, but that could simply be because they're not used to think in generic terms about sum types, or because they don't care about algebraic data types in general (I can say they would be missing a lot). But if one does, I don't see how they could not perceive the potential of these union tuples, given the already visible power of regular tuples.

14 Likes

Looks neat, did you consider making it a fledged pitch?

Hi @ExFalsoQuodlibet, thanks for taking the time to give a few examples to discuss.

There's a few examples similar to

func handle(
  _ event: (
    action: Action |
    change: Change
  )
) { ... }

given only action or change could be present for a particular invocation, how would the implementation look for these functions? In particular what's the treatment of the value that wasn't provided? e.g. if action is provided what is the value of change? Seems like they'd have to be optional and the function body would need to handle them as such.

The reason that Swift proposals make use of motivating examples is not merely because "everyone loves examples." Swift is a pragmatic language; this means that its features are meant to help users solve actual, real-world problems, and its design is meant to encourage users choosing the best solution by default. Any theoretically sound idea must nonetheless accomplish these practical goals in order to be fit for inclusion into Swift.

With this in mind, let's evaluate what you've laid out here in terms of examples and what they reveal about any possible design for anonymous sum types, starting from the bottom:


What you demonstrate here is that an ergonomic implementation of this feature would require sophisticated subtyping relationships.

Here, you are stating that (Action | Change | SideEffect) must be considered a subtype of (Action | Change); by induction, too, you'd need—and want—Action to be a subtype of (Action | Change).

To complete the feature, you would want sort of equivalence between (Action | Change) and (Change | Action). This would be so both for theoretical (and user expectation) reasons, since semantically the operation is commutative, and for practical reasons, since you don't want users to have to go through a dance every time one API returns (A | B) and another takes an argument of type (B | A).

We have an ad-hoc tuple shuffle feature that allows a value of type (a: Int, b: String) to be assigned to (b: String, a: Int). @codafi has been trying to get rid of it for a very long time; as he writes, just that feature alone "complicates every part of the compiler stack."

Put simply, there is no way to create what you call "a more powerful type system, without renouncing simplicity and accessibility." It is why the core team has said of this feature:

Disjunctions (logical ORs) in type constraints: These include anonymous union-like types (e.g. (Int | String) for a type that can be inhabited by either an integer or a string). "[This type of constraint is] something that the type system cannot and should not support."

Fundamentally, there is nothing revealed here that would surmount this barrier; what you imagine here is not possible to implement as part of Swift's type system.


Could there be advantages for the author of the API in allowing another way of organizing code? Sure. But we must consider the user of the API, and here's where things fall apart.

Now that we've established that the type system relationships outlined above are impossible, that leaves explicit type conversions at every point of use. This is wildly unergonomic for the user of the API, not to mention visually noisy. Consider:

// Using overloads:
event.handle(action)
event.handle(change)

// Using anonymous sum types:
event.handle(.action(action))
event.handle(.change(change))

Moreover, for equivalent performance, the compiler would have to optimize the code so that wrapping a value in a sum type and then unwrapping it are no-ops. Currently, they are not no-ops.

This is a good example of how a feature might cause a negative effect on other aspects of the language. In this case, adding what seems like an equally attractive (or perhaps more attractive) option for authors of the API that ends up being worse for the end user would be opposite to Swift's goal of encouraging the best solution by making it the easiest solution.


Consider what would happen if you'd try to model this type and there were milk, cookies, and bread. The absurd result that you'd obtain clearly demonstrates that this is not how we want to model a 'non-exclusive or.'

If we wanted to model these type relationships for the uses you'd describe, then it'd be perhaps appropriate to consider Milk ^ Cookies as the more accurate notation where you need one or the other, while Milk | Cookies would allow one or both.

Again, though, you're describing a type system that cannot be implemented in Swift.



We tried to do this once with an explicit type, ArithmeticOverflow. We had to remove this type in a revision to the proposal after the feature shipped. The reason was that, although self-documenting, the user experience was extremely silly.

This was because there is nothing you can do to generate such a result except to take a Bool and then convert it—and even to do that in one line, you'd want to create an initializer for your type, which you can't do for an anonymous type. There is also nothing that you can do with such a result except to convert it to a Bool. So what you're left with is instantiating something you immediately then discard—and, as mentioned above, this isn't optimized away, so you are paying a performance penalty for it too.

It's a good example, actually, of how Swift aims to be pragmatic, and what that means. On paper, it seemed like a decent idea to create ArithmeticOverflow for reasons similar to the ones you've outlined here; in practice, it failed to improve the user experience.


OK, I've written enough, and there are other things requiring my attention. I hope that this post has given some pointers both about the high-level design philosophy of the language as well as some clarifications of what is and isn't possible to accomplish with this feature in particular.

5 Likes

I guess you would handle it just like an enum with two cases with an associated value each: with a switch statement/pattern matching.

It's extremely easy to come up with reasonable type syntax for writing anonymous sums and fairly difficult to come up with reasonable expression/pattern syntax for injecting and projecting values in and out of those types. The most natural syntax by analogy with tuples would be to use integer case labels (.1, .2, etc.), but this problematically clashes with floating-point literals. Many people who want anonymous sums seem to want both injection and projection to be implicit, which is very much not analogous to tuples and would cause quite a few type-checking problems. You could require labels to be written out in the type, although it would make the types a lot less concise. But instead of discussion of these issues, I hear a lot about math, as if the only reason to be wary of the feature is that people are unaware of type algebra, even when people are manifestly not unaware of type algebra.

I'm starting to lean towards providing an Either in the library. Defining a real enum would usually be the better choice for an API, just like defining a real struct is usually better than using a tuple, but it's very convenient in implementations to have a type at hand with the right conformances and so on. The downsides of the type (just two cases, meaningless first/second labels) probably provide the right push towards real enums.

A variadic OneOf seems to have worse usability problems than numeric labels. Would all the cases be named the same thing? With due respect, that feels like abstract box-checking rather than putting any real thought towards design.

8 Likes

In other languages I've seen Either be used largely where Result would be used. What are the common cases where Either would be used? In those cases is it worth losing the improved clarity that a custom Enum brings? One of the main benefits of having a type in the standard is that packages can interoperate using that type. If it turns out that Either is generally used in application code that benefit no longer stands.

The event value is essentially an enum, and could be treated as such, switching over it.

No, I don't demonstrate that, in any possible way.

Absolutely nowhere I'm stating that.

The reason why the addition is backwards compatible is that the user of the api will pass either .action(someAction) or .change(someChange), which are also accepted after the addition of SideEffect.

I would not want that, in any possible way. I suspect that you completely misunderstood my entire post.

You are literally using a problem in Swift that exist right now to point out a potential problem related to what I'm proposing, save for the fact that I never proposed to problem to be added to the language.

You completely misunderstood what I'm imagining here. I linked the "commonly rejected changes" to ask for clarification a few posts earlier, and I got a clarification about it: it's related to implicit conversions, something that I'm not proposing in any shape or form.

This is simply not true. I use things like these all the time. It's unlikely that you'd write something like

event.handle(.action(action))

this would happen if you put your Action in the action constant. It's more likely to have either the Action to be an enum itself, or a struct with static functions or constants, such that you would still write Action explicitly in the overloads case, but you wouldn't in the other. The only problem would be in cases where you're constructing the action, calling Action( at that point, in a way that would read .handle(.action(Action(, but it really seems a minor problem to me.

Anyway, I agree that there's room here for writing bad APIs, and that in some cases the overloading could be better (but still, it would be harder to discover for the user).

This is a little uncharitable: I agree that there can be APIs that would be worse off in using anonymous sum types, but doesn't mean that all possible APIs would be, and definitely doesn't mean that a feature like that will encourage bad APIs.

I agree that, for the specific case of "non exclusive or", tuples and anonymous sum types are not perfect, but we certainly don't have anything in Swift that would work right now. Anonymous sum types allow for acquiring "some more power", but not all the possible power that ideally could be there. An anonymous non exclusive or would be great (though not nearly as useful and ubiquitous as the exclusive one), but it's not what I'm talking about here.

You are literally responding to a perfectly valid and useful feature that anonymous sum types would unlock (consider that there could also be more that 2 cases) with a single example where it didn't make sense, and where you already had access to a label (in the tuple) that conveyed the meaning (in fact the case name was the same as the tuple label). I leave to you to think about how strong is your argument in this context.

Here's another example straight from one of my codebases, where of course I defined an enum just for that case (and I converted it here in an anonymous sum type):

extension User {
  var consentRequestAnswer: (yes: | no: | didntAnswer:)
}

I definitely agree: the syntax is a delicate matter here, but I'm sure we could eventually come up with a decent solution if we decide to accept the utility of these types.

Not sure how projection could be implicit, but for injection I can see the point, and in fact this is generally what languages that have union types (like Typescript or Scala 3) end up with. But it's not what I'm proposing here, and I didn't see in this thread anyone asking for implicit conversions (I might have missed someone, though).

I agree that this discussion is important, but my intention for now is to explain and justify the utility of such a concept as anonymous sum types without implicit conversions.

I disagree that the only consequence of the awkwardness of Either is to push towards nominal enums: it also pushes towards anonymous sum types in the flavor I described. The awkwardness of a generic Tuple type would push towards actual tuples, and not nominal structs, and is the same for sums.


I'd like to underline that my intention, with my post, was not to propose a design for anonymous sum types: I clearly stated that I used a strawman syntax, and that the specifics should be discussed. My point was to show their utility, with the duality with tuples as a guideline.

3 Likes

I use an Either type in a few libraries I’ve written and I use something akin to OneOf (I call it Poly1, Poly2, Poly3, ...) in a library as well. My motivations for such types (despite the loss of a concise way to construct values of such types as if you were just using an anonymous sum type) are:

  1. You don’t lose type information in subsequent generic contexts like you would if you erased the type.
  2. I employ a fallback strategy to define decoding of the Poly type in terms of each possible type it could hold (in order). I don’t know how broadly applicable this would be, but having this shared Codable conformance is the primary reason why I use Poly instead of one-off enums that would have more descriptive case labels than Poly does.

Anonymous sum types would be, by their nature, anonymous; therefore, adding an additional case would create a distinct type. This is a source-breaking (not to mention ABI-breaking) change unless you have some sort of subtyping relationship between one anonymous sum type and another (or an 'extensible' anonymous sum type, which would be equivalent for these purposes).

let argument: (action: Action | change: Change) = .action(action)

event.handle(argument)
// error: cannot convert value of type
//   '(action: Action | change: Change)'
//   to expected expected argument type
//   '(action: Action | change: Change | sideEffect: SideEffect)'

For reasons I outline above, your example above regarding API stability is not possible unless there's some sort of subtyping relationship that's supported, which there cannot be in Swift.


It's very much true that the ergonomics of working with cases with payloads never been ideal. One needs only to try implementing a JSON type using an indirect enum; it's an exercise that descends quickly to absurdity if you're not careful, and the user of such an API wouldn't have a good time at all.

To be sure, it's more straightforward to create a case with a payload than to extract it, and there are a large number of conveniences that the core team has pointed out should be considered which deal primarily with working with an existing value. However, what's wrapped has to be unwrapped, so ergonomic issues with payloads are all of a kind.


It is more an empiric question, whether a feature promotes or detracts from the overall goal to steer users towards the best solution. This is why I worked through the examples you provided. If there is a "killer use case" for the feature, then it should be possible to discover it. That we have seen many examples that are worse off using this feature, and none seem to fit the description of a "killer use case," raises concerns.

It isn't enough just to have lots of examples where a feature could be used; a successful proposal should have examples (preferably many, and preferably obvious) where the feature could be well used.


I didn't cherry pick; I'm working through your examples in reverse order (since the last examples were the freshest on my mind).

As to your new example: another way to express the result would be var isConsentApproved: Bool?. In general, nil is a good way to model the lack of a value.

Yes, it is important to consider that you may have use cases with more than two or even three cases. In the case of tuples, though, we've discovered that the maximum ergonomic number of elements is fairly low, past which one is better off creating a real struct. Is it possible to come up with a situation where four or five cases are more ergonomic as an anonymous sum type rather than a named enum? Possibly, but it starts to become quite iffy, if we use our experience with tuples as a guide.


Whether we accept or reject this feature has to be contingent also on whether we can come up with a workable solution to these very practical issues.

It's related to the point I made earlier about looking at examples as a key part of evaluating a proposal; we shouldn't be making a decision on including a feature based on abstract considerations, then trying to figure out if we can.

As I said earlier in this thread, I would want implicit conversions if we had anonymous sum types. This is also not the first thread about this issue, and many people have wanted the same; it's "commonly proposed" for a reason.

2 Likes

By allowing implicit conversion to a common type of the components, including e.g. calling methods they all provide with compatible signatures. It’s a language model less like an functional ADT and more like an ad hoc supertype.

One possibility I could maybe imagine would be to treat this less as "sum-equivalent to tuples", and more as "Any, but with more hints to the user/compiler".

Creating such a value would thus be implicit—just like conversion to Any is. Getting a value of specific type out of one would work with as? or a switch with patterns querying the type.

This way, the order of the types in the declaration doesn't have any impact, but you could still get benefits like potentially better performance, more clarity of APIs using such a type and exhaustive switches over the different possible types of values.

Is that something that could be done without taxing the type system too much?

3 Likes

There some complicated design ideas floating around here. To be honest with you, for me such a type just need to do a few things:

  • support implicit wrapping (like Optional), but only if there is no ambiguity, otherwise require an explicit indexed case constructor
  • support for exhaustive type matching only when there is no ambiguity.
  • OneOf<Int, Int> should remain a valid type, regardless its usefulness. You should be able to switch over the indexed cases.

To sum up, I really only need an enum with a variadic generic type parameters and cases which I can construct and pattern match with index based case names. Everything else, including some points mentioned above, is just nice to have, but not mandatory.

let value: OneOf<Int, Int> = .1(42)

switch value {
case .0(let first):
  ...
case .1(let second):
  ...
}

You're right, but I don't expect that users would explicitly put values of type "anonymous sum" in variables, and then pass them to functions, so this problem wouldn't be frequent. In general, though, you're correct, and I understand that what I wrote could have been misinterpreted.

You're right, but this is a completely different problem to what we're discussing here. The table I've shown earlier points out the "extraction problem", among other things. But I disagree with your remarks on the ergonomics of enums: if you want to be expressive and safe, it's inevitable that you're going to use more syntax than, say, just defining things with strings.

For example, in the json case, an implicit conversion of this kind (via ExpressibleByStringLiteral):

let someJson: JSON = """
{
  "foo": 42,
  "bar": [
    "1",
    "2",
    "3"
  ],
  "baz": {
    "subfoo": "value",
    "subbar": 43.43
  }
}
"""

is cool, but I would never use it, because it's unsafe. Something like this, though:

let someJson = JSON.dict([
  "foo": .num(42),
  "bar": .list([
    .str("1"),
    .str("2"),
    .str("3")
  ]),
  "baz": .dict([
    "subfoo": .str("value"),
    "subbar": .num(43.43)
  ])
])

while definitely heavier syntax-wise, is good enough, and 100% safe. I guess we disagree on the ergonomics.

The judgment of "well used" and "killer use case" are rather subjective. To me, both the examples related to heterogeneous collections and on-the-fly tagged unions for return types are killer use cases.

I've used Bool? to model this in the past, but then I understood, together with many of my colleagues (even Kotlin people), that Bool? is usually really bad, because it doesn't mean anything in itself. The premise of all this is to use types to better convey meaning, and there is great value in using a specific enum in place of something like Bool? (or even just Bool, in some cases) to better express some state of the system. The problem is the need to always explicitly create enums for each case, while if the return type is a product (instead of a sum), we conveniently have tuples: the duality here shows pretty strongly that Swift lacks something that would be equally useful.

I disagree with this, and I don't see how the number of cases could guide in any way the need for a named enum. It's really the same for tuples: it would be like saying that if a function has more than 3 parameters, then it's better to define it as a function that takes 1 parameter, which is a struct with all potential parameters as properties.

Seriously, I don't see (and never saw in my experience) the need to use a struct instead of a tuple just for the number of parameters: I use structs when there's a reason related to domain modeling, that usually goes hand-to-hand with the need for methods and protocol conformances. So, to me, in this discussion, the number of parameters/cases is an irrelevant factor, and I think it would be up to you, if your criticism is along the lines of "we don't need this because more parameters require a nominal enum", to actually show why it's the case.

An example, related to tuples, where "many parameters != need for nominal type" is when zipping things: for example, with Result, the fact that I'm zipping 5 Results doesn't mean that I should define some silly struct called FiveSuccesses or something.

While I agree that something like what you propose would be better than just Either<A, B> added to the standard library, it would be far off from achieving "feature parity" with tuples (not that you're asking for that, of course). In particular, the possibility to add case labels on the fly, as I think I showed in the examples, would be of great value.

1 Like

I think that this is the main selling point for me. I do tend to go for Optionals and Bools more often than I'd like because the weight of defining a new enum doesn't seem worth it when a function wording can make it clear enough what the values mean. But a sum type would be clearer still