Adding Either type to the Standard Library

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

Clashes how? Swift has this error, no?

let x = .0
// 🛑 '.0' is not a valid floating point literal; it must be written '0.0'
2 Likes

Oh, looks like I misremembered, sorry. That said, even if this is technically allowed, I think it's going to read badly. Tuple projection syntax is at least always a member access with a real base, e.g. t.0; this would frequently be a contextual member access, e.g. .0(payload).

This is exactly what I was talking about when I said:

I have had to define a family of AnyOfN types to support composition in a library I work on. The case names are a1, a2, etc. These types are transient values produced by composition that are either used immediately or transformed in some way. I'm not aware of a better solution than this currently. A language-provided solution that is consistent across codebases and libraries would be much preferable IMO.

3 Likes

Possible funny implementation of NonEmptyArray by Either :smiley:

enum ABC {
    enum Either<Left, Right> {
        case left(Left), right(Right)
    }
    
    struct NonEmptyArray<T> {
        var array: [T]
        var first: T { array[0] }
    }
    
    struct EmptyArray<T> {
        var array: [T] = []
    }
}

extension Array {
    var eitherArray: ABC.Either<ABC.EmptyArray<Element>, ABC.NonEmptyArray<Element>> {
        if isEmpty {
            return .left(.init())
        }
        
        return .right(.init(array: self))
    }
}

Union is just a set of types, and I don't think it should be a sugar of enum. Therefore, I am against the idea of putting a label on it, much less allowing only a label. Also, there is no order to the set, and I think it's nonsense to access it with .0 and so on.

Now, (A | B) is a super type of A. In light of this, I suggest using as to extract the actual type from the Union.

let someRange: (Range | ClosedRange) = 0...1
let closedRange: ClosedRange? = someRange as? ClosedRange

In this case, it is still possible to check the types comprehensively.

switch someRange {
case let x as Range: ...
case let y as ClosedRange: ...
}

I think Union in Swift is a neat contrast to the tagged enum, and it overcomes Existential's performance problems (though not as generic as Existential). I'm not sure what the problem is with the type system, but I'm hoping this feature gets into Swift.

5 Likes

Instead of making "|" a keyword punctuator, my ideas on this used a comma to separate, and new token sequences to open/close a sum-tuple:

(; field1: Int, Double, field3: String ;)

and it gives us the option of zero- (or one-) element sum-tuples.

Until this thread, I never saw a sum-tuple idea with just a label for a field. What would be the difference in that over using a Void value of the field's type?

I take it that most of everyone here wants sum-tuples (at least by default) be tagged for each field, just like nominal enums, right? Some of the sum-tuple ideas I had included a variant that is a strict C-like union, no tagging and assumes the developer knows all the fields' types are compatible bitwise. Maybe we could still have that kind of raw union, but only if we could confirm that every field type shares representation at the IR-Gen level.

For access, we would assign to a field to write (i.e. inject) and access fields as Optional to read (i.e. project). A read field returns nil if it isn't the active case.

I think it’d be better this way:

let someRange: (Range | ClosedRange) = 0...1
let closedRange: ClosedRange? = someRange as ClosedRange?

I say that because OneOf will always be a super type of Optional, but not always Wrapped.

I agree that “|” may not be ideal, as it creates confusion - especially to beginners - since a tuple:

let a = 5, b = 5
let tuple = (first: a | b)

In the example above the expectation is that the Bitwise “|” operator will take the values a and b and return another integer value. Therefore, using “|” instead of commas (“,”) is in my opinion not the best choice. I think that instead of using parenthesis (“(“, “)”) we could use “|” and use commas normally:

typealias T =  | a: Int, b: String |

I don't want to use "|" at all, since any use would need to move it from being a identifier-class punctuator to a keyword-class one. I don't see why we need it as a cute mnemonic for OR-ness since we don't do the same with "&" or "*" for product-tuples. And could the parser handle the same token being used to open and close a new bracketed construct? I use "(;" and ";)" because they use punctuators that are already keyword-class, are currently illegal combinations (i.e. open to be repurposed), and hopefully easy to parse.

The current syntax for down cast is as? T. Union should have the same syntax as it, and IMO, it looks more correct.

class A {}
class B: A {}

let a = A()

// success
let b1: B? = a as? B
let b2: B  = a as! B

// error
// cannot convert value of type 'A' to type 'B?' in coercion
let b3: B? = a as B?
enum Either<Left, Right> {
  case left(Left), right(Right)
}

Let’s not allow the great be the enemy of the good. Let’s sink the above into the standard library. I guess it’s already there so let’s just remove the underscore.

1 Like

It's a degenerate case and shouldn't be allowed by the compiler, by it could work in theory: to construct a value for that case, () should be passed explicitly, for example:

typealias Union = (valid: | degenerate: Void)

let good = Union.valid
let bad = Union.degenerate(())

I definitely do, I think there would be great value in that.

It also seems that discussing the possibility to add an actual union type with subtyping and implicit conversion is not useful, because it was already ruled out by the core team. Also, it's a fundamentally different discussion from anonymous enums in the same flavor as tuples.

I agree that using a (_: _ | ...)pattern for union tuples is not great. Maybe using square brackets could be better?

[Int, String]
[case1: Int, String]
[Int, case2: String]
[case1: Int, case2: String]
[case1:, case2: String]
[case1: Int, case2:]
[case1:, case2:]

There would be ambiguity only for single case union tuples without a label, like [Int], but this is degenerate and shouldn't be allowed by the compiler (in a similar way as to how Swift doesn't allow single-element regular tuples).

Not sure what was your intent with this code, but it makes no sense: an instance of a union tuple can only be created from a single case.

1 Like

The Topic of this Thread

In this thread there are a lot of great ideas. The problem is that they discussion is too broad, so I think we should narrow it down in this particular thread. I think Union Tuples would be a really useful feature; first, though, we should discuss about adding an Either type to the Standard Library. Because, if we don't have any insight into how Either will be used “in the wild”, we could make bad design decisions when designing Union Tuples.

Possible design of OneOf

Nonetheless Varidadic Generics are required to make an OneOf type. That type could combine some ideas proposed upthread. Namely, it could have:

  1. A "|" operator, to create OneOf types - like ? for Optional:

     typealias MyOneOf = Int | String
    
Note

The following implicit injection/projection are similar to Hashable's

  1. An injection mechanism:

     let myOneOf: MyOneOf = 5
     // equivalent to:
     let myOneOf: MyOneOf = .init(choosing: 5)
    
    Implementation
    public struct OneOf<variadic Values> {
        @usableFromInline
        internal let _value: Any
    
        @inlinable
        public init<Value: Values>(choosing value: Value) {
            self._value = value
        }
    }
    
  2. A subscript projection mechanism:

     let a: Int? = myOneOf as? Int
     // equivalent to:
     let a: Int? = myOneOf[Int.self]
    
    Implementation
    extension OneOf {
        @inlinable
        public subscript<Value: Values>(_ type: Value.Type)
              -> Value? {
            _value as? Value
        }
    }
    

One problem that could be quite problematic in the above is the fact that OneOf<Foo, Foo> would be valid. Therefore, I think that an advanced variadic generics system will be required for an implementation as shown above.

Focus in Either

Despite, being amusing to think of future Union Tuple designs it is currently little more than a waste of time. For now, we should focus on Either case naming ("first"-"second", "a"-"b", "left"-"right"), projection mechanisms and default conformances.

1 Like