Adding Either type to the Standard Library

The commonly rejected change, as I understand it, is specifically about union-esque types (logical OR), which is slightly different from anonymous sum types (closer to logical XOR if you want to extend that analogy). Indeed, the note explaining why it won’t be accepted says that it is “something the type system cannot and should not support,” but the type system clearly can (and should!) support anonymous sum types—they are just enums with less characters.

4 Likes

To be clear, I do not think an Either type in the standard library is the right direction. I want language support for anonymous sum types.

As @Jumhyn explained, anonymous sum types are much different than union types. To my knowledge, anonymous sum types have not yet received serious discussion and consideration by the community.

10 Likes

I'm not sure, and it's easy to confuse union and sum types…

The union type from, for example, C++ does not seem like a good fit for Swift to me, but a "tagged union with hidden tag" (see Ceylon for an example) is extremely powerful: The use case for sealed protocols could not only be modelled with tagged unions — imho it's actually the better way to do so.

Enums are quite different, and I have no idea how exactly anonymous enums should work.

2 Likes

The Either<A, B> type is exactly as important as (A, B), no more, no less: for example, being able to "return A and B from a function" is exactly as important as being able to "return A or B from a function". These are dual constructions, there's no reason to favor one over the other in terms of utility: sometimes I want to milk and cookies, other times I want either milk or cookies.

That being said, to me the ideal case here would be to have anonymous sum types, like (A | B), that could be inspected by index, with .0 and .1: and of course, the importance of (A | B) is exactly the same as (A, B).

EDIT: I might have come out a tad too aggressive here. That's not my intention, I understand that not many are used to think about things in Either terms. I just want to highlight the fact that there's an objective dualism here between the constructions, that maps to a dualism of meanings.

7 Likes

We could hope that plenty of people are used to think about things in Result terms, which is basically typealias Result<A> = Either<A, Error> :slightly_smiling_face:

2 Likes

small thing but can come in handy to have a clear contract that you always have one of two values

Sorry, I misunderstood.

Would you elaborate on what exactly you are proposing here. Does the following seem like what you are looking for?

let a: A | B | C = A()
// Equivalent to:
let oneOf: OneOf<A, B, C> = .choosing(A())
Note:

In the above I just think that OneOf should be there as Optional is the actual type behind an expression such as this: func foo() -> Bar? (== Optional<Bar>).

Sum Types Direction

In the above example of in what I assume you mean by sum types, we could do something like:

let foo: Any = oneOf.value // A value of A, B or C

or:

let bar: A? = oneOf.value

or:

let bar: A? = oneOf[A.self]

to access the value. We could also do:

oneOf.0 // A?
oneOf.1 // B?
oneOf.2 // C?

Another alternative (same as the above with subscript syntax):

oneOf[0] // A?
oneOf[1] // B?
oneOf[2] // C?

IMHO, the following should be chosen if we choose to go down this path:

let foo: Any = oneOf.value // A value of A, B or C
let bar: A? = oneOf.value

oneOf[0] // A?
oneOf[1] // B?
oneOf[2] // C?

Does any of the above feel appropriate?

Common control flow structures

  1. Switch statements:

    In the Ceylon example that @Tino pointed us to, the compiler seems to do the following:

    void printType(String|Integer|Float val) {
        switch (val)
        case (is String) { print("String: ``val``"); }
        case (is Integer) { print("Integer: ``val``"); }
        case (is Float) { print("Float: ``val``"); }
    }
    

    which would translate to the following in Swift code:

    switch oneOf {
        case is String: print("String: \(value)")
        case is Int: print("Int: \(value)")
        case is Float: print("Float: \(value)")
    }
    
  2. If/Guard statements:

    I think the following is a valid choice.

    guard let a: A = oneOf else { ... }
    

    Alternatively - with no compiler magic:

    guard let a: A = oneOf.value as? A else { ... }
    

I would expect the opposite to be true—anyone who uses enums with associated values in Swift is very used to thinking in Either terms, even if they don't call it that.

The key difference is that use-case-specific enums provide valuable information in their labels, and possibly in their constraints; as @Max_Desiatov points out, Result is essentially Either where one of the values is constrained by Error, and its labels success and failure make their usage clear vs. more generic terms.

This feels like a bit of a contradiction. What some folks, including myself, are claiming is that that Either<A, B> specifically is less important than (A, B) because it only considers the 2-element case, whereas both (A, B) and a hypothetical (A | B) are generalizable to N elements. It would be akin to having Pair<A, B> in the standard library.

10 Likes

Core Graphics isn't a Swift library, and even if it were, I'm not seeing how CGImageAlphaInfo lends itself to an Either type. How exactly would you redesign the type to fulfill its current role?

You'll have to offer at least some specifics: great potential for what? with what compiler magic? I'm not seeing a clear use case outlined here.

As @anandabits has written, he's arguing for anonymous sum types.

Omission of Either 'initialiser'

Firstly, I think we should consider something similar to Optional:

func foo() -> Either<Bar, Baz> {
    myCondition ? Bar() : Baz() // ✅ 
    // Equivalent to:
    myCondition ? .choosing(Bar()) : .choosing(Baz())
}
choosing(_:)
static func choosing(_ value: First) -> Self { .first(value) }
static func choosing(_ value: Second) -> Self { .second(value) }

This would be a great way for a hypothetical type such as Time:

enum TimeDescriptor {
    case aLongTime, aReallyLongTime, aLittleTime
}
typealias Time = Either<Double, TimeDescriptor>

wait(for: .aLongTime)
wait(for: 0.3)

Or for something like Font Weight (I'm not going to bother you with the implementation):

setFontWeight(to: .bold)
setFontWeight(to: 600)
setFontWeight(to: myCustomDoubleValue)

Automatic Protocol Conformance

Another more advanced feature could be automatically conforming Either to protocols - or classes if both the first and second type are classes - that both types conform to. So that this would be possible:

protocol Foo {
    func foo() { ... }
}
struct Bar: Foo {}
struct Baz: Foo {}

let myEitherValue: Either<Bar, Baz> = ...

myEitherValue.foo()

So, in the example above, with time, if both Double and TimeDescriptor conformed to hypothetical protocol Operatable - which would provide basic operations, such as +, -, /, *- we could write:

wait(for: .aLongTime + 0.5)

The pointfree guys discussed this idea in Episode #51: Structs 🤝 Enums back in March '19. They even proposed some syntax that looked like tuples with | replacing the , between fields. That's a subscriber-only link, I'm just highlighting it to say that it has in fact received discussion in some corners of the community. I'm a big +1 on the entire idea as a result that discussion.

4 Likes

Thanks for sharing that link here, I was speaking about SE specifically and not the broader community. @stephencelis has been the most vocal advocate of putting sum types on equal footing as sum types in the language. Point Free has several ideas in this direction, all of which are great!

4 Likes

I agree. What I meant is that people might be less inclined to think in completely generic terms about a value that's either something or something else, without domain-specific names attached to cases.

I agree, 100%. A generalizable (_ | ...) syntax would be great. And yes, Either<A, B> is definitely less important than a generic n-value tuple (but is exactly as important as the specific 2-tuple).

Unfortunately, in the commonly rejected changes it is stated that anonymous union types are not going to be ever supported: I'm not completely sure how to interpret that paragraph (or if the core team changed their mind here), but if there's was no way to ever get (A | B), I'd rather have at least a Either<A, B>, and possibly a bunch of them with different arity, in the standard library.

Anyway, my more general point is that sum types are as important as product types, and I would expect every feature and construction available to the latter to be also, dually, present for the former.

2 Likes

(A | B) is just strawman syntax used in this thread for what could be a variety of concepts. A true "anonymous union" type would be one that allows the user to assign a value of either type A or B to it without explicit conversion and would likely also support the common set of properties/methods from both of those types, again without any kind of explicit casting or qualification. I believe that's what that section of the document is referring to.

An anonymous enum expressed using something like numbered cases analogous to unlabeled tuples (case .0(let foo): case .1(let bar)) and requires explicit pattern matching to extract the values would not be the same thing.

3 Likes

You're showing me features that can be added to Either.

However, my question is, what is the "great potential" unlocked by this type? In other words, I'm asking why use (not how to use) Either in the first place: in what specific scenarios would this type represent the best solution?

Such a design requires users to wrap the argument in an Either type before passing them to a function named wait(for:), which either requires the compiler to optimize the wrapping and unwrapping away or imposes a runtime performance cost. Instead, we can already allow users just to pass the values themselves to one of two suitable overloads, which is a superior way to design this API, being more straightforward to use and also more straightforward to compile. In this scenario, Either would be an attractive nuisance rather than a feature.

This use case seems to call for a garden variety custom enum FontWeight { case extraLight, light, regular, bold, extraBold, custom(Int) }. I don't see how Either would yield a superior result here either.

If you have a reason for preferring Either in these examples, I'd love to explore the specifics of why. Again, it's important to delve into fully fleshed out use cases; do bother with implementations.

This isn't theoretically sound. A length can be added to a length, and a time interval can be added to a time interval, but that doesn't mean that a length can be added to a time interval, or a dollar amount to a weight. For the same reason, protocols with associated types cannot self-conform. In the case of such a protocol P, what you describe would cause Either<P, Never> to be equivalent to a self-conforming protocol existential type.

3 Likes

The need for unions can arise naturally when zipping, depending on the structure of the type that is zipped. As with the tuples produced by the zip that we already have, a further transformation is often applied. I work on a library that currently uses AnyOfN types in this circumstance. I would greatly prefer not to need these types. I would even more greatly prefer to be able to write the operators that produce these types in a variadic way. That is unlikely to be possible with user-defined nominal types even if we have variadic generics in the language.

3 Likes

As a (semi-)aside, the "union type" on the commonly rejected changes list is about a type with implicit conversions, which is a little different from the anonymous-but-explicit enum/union/sum types advocated by Pointfree and others. I wish I could find a citation for how they'd complicate the type system, but I can't, although there's a little discussion way back in an early thread about Result and Either.

TypeScript's union types allow accessing members common to all parts of the union. Swift is unlikely to support that because the low-level calling conventions for different types don't match up, and besides, Swift doesn't consider two methods with the same name to be "basically the same operation" unless they're on types conforming to the same protocol.

Ceylon's union types don't allow accessing any members ad hoc, but do allow converting to a supertype of all members. (That is, UIScrollView | UITableView can be converted to UIView.) This can still have strange behavior when interacting with Swift's generics: if I have an [Int] | Set<Int> value, and I pass it to a function that takes any Collection via generic parameter and returns an index, what's the type of the result? Or is that just not allowed?

Even if your anonymous enum/union/sum type requires explicit deconstruction (i.e. "you have to pattern-match it to do anything"), there's still the question of whether you'd allow implicit conversions to the type. Even that adds additional complication in Swift's type system, though, which allows inferring types across an entire expression. While Swift's type checker has gotten a lot better over the last five years, implicit conversions are still one of the ways to make it do exponentially more work if we're not careful how it's defined.

There may also be additional complications I haven't thought of; maybe one of the type system folks will know what John and Chris were referring to in those early comments about "massively complicated the type system". But I'm pretty confident that the explicit form where you say .0(x) and match against case .0(let value) don't hit any of those problems. (Whether or not they're worth it is a different question.)

21 Likes

I agree. What I'd like to see is an anonymous sum (coproduct) type without implicit conversion, that's dual to tuples. I personally don't really care about (non-tagged) union types with implicit conversion a là Typescript or Scala 3: but I do care about the ability of using the same syntax and power of tuples (including optional labels) but with "either" semantics.

Thanks a lot for this clarification.

They're worth exactly the same as tuples are worth today in Swift, that is, a lot. This is one of those cases where there's really no point in showing examples, because we already have tuples, and everything that can be said for a tuple could be said for an anonymous either.

In my personal experience, one of the main applications is for sequences and streams, in which each element can be one of a number of options: being able to operate generically on such sequence, by folding it or partitioning it, for example, is of great value.

And in general, the idea of generically being able to return from a function A or B (or C...) has exactly the same value as being able to return A and B (and C...).

As Pointfree points out really, really well, Swift doesn't take advantage enough (if at all) of the duality between product and sum types, but there's not reason to favor one over the other, and similar (but dual) properties and constructions should be available for both.

The following table shows a few of those:

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

There are 5 spots here with a "No" inside, that in my opinion should all be filled with a "Yes", eventually: 2 of these spots would be set to "Yes" with the kind of "anonymous either" we're talking about here.

6 Likes

[citation needed]

Hard disagree.

How about,

“When a function returns a tuple, the caller can directly access and use members of the tuple because they have known types.”

?

That cannot be said about a sum type, because by definition—in fact the main feature—of a sum type is that it could hold any one of several possible types.

That is a hands-on, real-world difference. between sums and tuples. They are used for different things, in different situations.

Yes, mathematically they are structural duals. But we are not dealing with abstract mathematics here. We are dealing with an actual programming language, created, maintained, and used worldwide by real people.

The question at hand is, “Do the benefits to the users of the language from adding this feature, outweigh the costs to the creators, maintainers, and yes, the users themselves?”

In order to answer that, we need at least a general idea of what the benefits are to users.

Helping people understand those benefits is the purpose of examples.

Examples are necessary.

1 Like

Thanks for posting this table! It could be extended with an entry showing the CasePath dual of KeyPath. That is also a fundamental abstraction related to sum types that should have a place in the language and standard library. I think the popularity of The Composable Architecture demonstrates the utility and need for this very well.

Direct access is not the only thing that matters:

switch someFunctionReturningLabeledSum() {
case .firstLabel(let value):
...
}

switch someFunctionReturningUnlabeledSum() {
case .0(let value):
...
}

This may not be direct use of the components, but it is direct use of the function result.

There is no more reason to require the function to declare a nominal enum than there is to require functions returning a product type to declare a nominal struct. As I mentioned previously, there are times where generic code is not able to add any value in a name. This is common when composing values. It is why I have had to declare a family of AnyOfN types. It is a pretty clear gap in the language IMO.

Anonymous sums integrated with variadic generics should enable merging of any number of heterogenous streams of values with a single generic implementation. It should also enable writing a single generic implementation that can fold such a stream into a homogenous stream. Does that example help you to see the utility in real world programming?

3 Likes