Adding Either type to the Standard Library

I was providing an example of something which can be said about tuples but not about sums.

I do not see an example of what you claim.

1 Like

I don't think @ExFalsoQuodlibet meant "tuples and anonymous sums are isomorphic"—if they didn't serve distinct purposes there would be no point in having both in the language.

(@ExFalsoQuodlibet, I don't want to put words in your mouth, so please let me know if I've misinterpreted.)

I took the argument to be something more like:

  • As nominal types, sum and product types are equally "privileged". They both have their own declaration kind, which is about as "first class" as you can get in the language.
  • As anonymous types, though, products are clearly more privileged. We have tuple syntax for constructing ad-hoc product types with no clear analog for sum types.
  • Any argument for supporting tuples/anonymous products—despite the existence of nominal product types—also serves as an argument for supporting anonymous sum types.

The last bullet here is the point under contention, but it isn't really refuted by "you can access and use members of a tuple directly" because that isn't an argument that supports anonymous product types, it's an argument that supports product types in general.

2 Likes

I was thinking of suggesting this after making an Either type, having seen it in other projects, and considering the benefits reaped by gaining a standard Result type in Swift 5.

Like Result, it is trivial to implement, but facilitates composition when the same type is used in different frameworks and projects.

Of course they are. I never wrote that they're identical constructions: if they were, there would be no need for a new one. It's how they're different that matters, but their differences don't also result in differences in how they're relevant and useful: they are the same, in this regard.

I clearly stated over and over again that they are dual constructions, that serve dual purposes. I also pointed out, in fun words, the key argument in favor of anonymous enums: generically representing A and B is not more powerful nor more useful than generically representing A or B.

I also noted that

but this is mainly because "traditional" languages don't really consider this pattern of thought: but "traditional" languages don't even have sum types, and that's a major problem in them, and you don't need many concrete examples to show that a data structure that statically represents either A or B is useful, and correctly models many domains.

Almost everything in a programming language comes from abstract mathematics. Type theory, within which type systems are (rigorously) defined, comes from abstract mathematics, and so is the abstraction power that comes from generic types. Abstract mathematics is not just a good guideline when discussing the sophistication and the features of a type system, it's the best guideline, founded on a huge, extremely useful body of work. And many real people worldwide use abstract mathematics to solve real problems.

I agree that practical examples are useful, though: the fact that a construction exists in abstract mathematics doesn't mean that it's going to be particularly useful in a programming language. But the thing we're discussing here is a very special case in the realm of possible constructions, because the language already has something that has the same power, but in the dual sense. This is akin of discussing if we should add the || operator in a language that already has the && operator: the one we're discussing is simply less common, but a very similar problem from a theoretical standpoint.

Of course, if it turns out that implementing this anonymous enum is a massive deal, and I'm no expert there, this feature is probably unlikely to get high priority due to the fact that we can already emulate it with Either2, Either3, Either4 et cetera (this would be the same if we were discussing the addition of tuples, while being able to define Tuple2, Tuple3 and Tuple4 structs). But in terms of mere utility, we already have a very clear point of reference in how tuples are useful in Swift today.

As the Pointfree guys have shown in many episodes, there's all sorts of useful things we do with tuples and structs that could and should be done also for sum types, and the key argument (that I personally find 100% valid and relevant in itself, examples or not) is that product types are not more powerful or useful than sum types. We could really just leverage a simple analogy: structs are not more powerful or useful than enums, therefore anonymous structs are not going to be more powerful or useful than anonymous enums. Any argument - related to usefulness, not implementation details at the compiler level - against anonymous enums would be applicable against tuples. I understand that the lack of a negative argument doesn't equal positive argument: but we do have positive arguments in favor of tuples, and they're equally applicable to anonymous enums.

I'm certainly not saying that examples are irrelevant: they're extremely useful, and in fact, as I said, in my experience I found a very important use case in sequences and streams. I'm sure that there have been dozens of times where I wanted to represent an heterogeneous collection: the most precise way to do this, in Swift, is to define an enum for the possible types, and then define a collection of that enum: the problem is the necessity to actually define a nominal enum that has really nothing of nominal, and ends up being some EitherN type.

Another example is when dealing with constructions in which we might want to represent a subset of options (I'm pulling enums out of my codebases that wouldn't exist if we had anonymous sum types). Consider an Event type:

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

This has no methods, and labels have the same name as the types (a strong indicator that this nominal type is pointless). This exists because some functions can return a bunch of Events, that is, any of Action, Change or SideEffect. Unfortunately, some functions could return ActionOrChange, or ActionOrSideEffect: that forced me to define further enums for the specific 2-cases.

You're right! I forgot about 'me, let me add them real quick:

Products Sums
Nominal constructions Yes, struct Yes, enum
Field access on nominal constructions Yes, with properties No
Destructuring of nominal constructions No Yes, with case
Anonymous (optionally labeled) constructions Yes, with tuples No
Field access on anonymous constructions Yes, with index or label No
Destructuring of anonymous constructions Yes No
Hierarchical access and modification Yes, with KeyPath No

Thanks for understanding my point.

4 Likes

I'm neutral on the stance, but I think you and @Nevin easily talked past each other. For those that don't agree that duality == equal privilege, half of the argument* which is based on this premise amounts to nothing more than a cacophony.

Edit:
* There's still other half where you start to mention things that wouldn't exist should we have anon-sum type. You also mentioned Pointfree episodes, which I'm rather interested in, but most of their videos are behind paywall (not to criticise their business model), so it's not exactly publicly accessible.

1 Like

To clarify, I am not arguing the merits of anonymous sum types.

I am pointing out the importance of examples, so that the proposal can become stronger.

If the motivation for a proposal is just, “This is mathematically equivalent to something we already have,” and the authors outright refuse to provide examples of how it is useful, then I guarantee the proposal will get rejected post-haste.

Thus, if the proponents of anonymous sum types are serious about wanting them in the language, then they need a strong proposal which can convince people who are not mathematically inclined, who are not type theorists, and who have not used anonymous sum types before.

The proposal needs to explain exactly why anonymous sum types are beneficial to Swift programmers, and that necessarily includes explicit, hands-on, real-world examples, where they are demonstrably superior to every alternative.

At the very least there needs to be a single well-explained “killer app” for the proposed feature, a “halo use-case” as it were, that the proposal can hang its hat on.

8 Likes

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.