Adding Either type to the Standard Library

None of that, IMO, is a reason to rush a feature that we already know will incur technical debt in the standard library the minute it lands. Instead, the fact that variadic generics keep coming up in discussions across numerous proposed features and that people want to try to work around the lack of them in different ways is a sign that variadic generics needs to be a much higher priority than it is today, so that other parts of the language can move forward more easily.

16 Likes

What? Why not?

When I refer to a OneOf type, I mean I mean a Variadic option type:

typealias MyType = OneOf<A, B, C>

to use MyType:

let myType = MyType(choosing: A())

To answer your question, I guess OneOf could allow that by requiring an index in its initialisation, but there are substantial drawbacks to doing that:

  1. A Tuple - (Foo, Foo) or just Foo - would be the correct way to do that.
  2. Features like "myType as? A", "MyType(choosing: A())" would not be able to be added in the future without source-breaking changes.
  3. It would be slower to do so - with run-time checks, and without compiler-errors it could be frustrating to get run-time ones.

(Foo, Foo) is not the correct way to model a tagged union of Foo and Foo. (Bool, Foo) can model it.

I don't necessarily think myType as? A is a good idea, but it's not clear to me why it's precluded (or slowed) by allowing OneOf<A, A>. Regarding MyType(choosing:), we already have an extremely similar situation in Combine's Result.Publisher. When the Output and Failure types are different, it just works:

  1> import Combine
  2> struct E: Error { }
  3> Result<Int, Error>.Publisher(E())
$R0: Result<Int, Error>.Publisher = {}

When the Output and Failure types are identical, the ambiguity causes a compile error:

  4> Result<Error, Error>.Publisher(E())
error: repl.swift:4:1: error: ambiguous use of 'init(_:)'
Result<Error, Error>.Publisher(E())
^

Combine.Result:9:16: note: found this candidate
        public init(_ output: Result<Success, Failure>.Publisher.Output)
               ^

Combine.Result:10:16: note: found this candidate
        public init(_ failure: Failure)
               ^

It's fine as long as there's some way to remove the ambiguity. In the Combine case, here's one way:

  4> Result<Error, Error>.Publisher(.success(E()))
$R1: Result<Error, Error>.Publisher = {}

Can somebody please explain, is there any reasons against making current _Either<Left, Right> public instead of internal?
In reality, developers create their own Either type. So, why can't we allow them to use native one, making it public?
I understand that there are some reasons, but it is interesting to know which one.

See my comments above. Something that's purely internal can change or be removed from the standard library; there's no requirement to maintain compatibility with it. But once it's made public and people start using it, it must be maintained potentially indefinitely, even if it's marked deprecated, or until the Swift team decides they're willing to break old code.

Adding public APIs to the standard library is not just about making things convenient for end users—it's a decision that has much longer term consequences for Swift's evolution.

Thanks for your answer. Yes, I keep that in mind. What I mean is that variadic generics and OneOf is an additive feature. Either<A, B> and OneOf(A | B) are not mutually exclusive, we can use both. Am I right, or not?

I don't want to say which of them is better. What I want to say, is that Either<A, B> is useful by itself. And its implementation is one line of code, just Either<A, B>.
To be more precise, the questions are:
Will Either type bring some troubles to implementing OneOf in the future?
Will any conflicts be having them both?

1 Like

I don't think I follow. What I say in my post is that Either<Foo, Foo> is not - logically - valid, as it provides an option between choosing one of two possible types, which are the same. In other words, it's like choosing one of 1 and 1; they're the same. So, if one wants to choose between two values of the same type - Foo in this case - they use a variable. The reason why I included the tuple type: (Foo, Foo) is because someone could have meant to write a tuple instead of a union tuple: (Foo | Foo).

As for the technical aspect, I don't understand how your type could be implemented. I've written a possible implementation of an OneOf type. Could you please show me how it could fit in to such a model or share an implementation of your own?

I thought you were talking about the Either type as described in your original post, but renamed OneOf. That Either type is a tagged union (also called a discriminated union, a sum type, a coproduct, the list goes on…) and allows both type arguments to be the same type.

But now it sounds like you are arguing for untagged unions, not tagged unions. In that case, I agree that OneOf<Foo, Foo> is isomorphic to Foo. But also in that case, IMHO you haven't met the “new information” requirement of the commonly rejected changes list.

As for “how your type could be implemented”, I'm not sure what “your type” refers to. If you mean OneOf<Foo, Foo>, then I was operating under the assumption that OneOf<Foo, Foo> is exactly the Either type of your original post (renamed from Either to OneOf). That Either definition is already supported by the language, so no new implementation is needed.

If you are asking how to model that Either<Foo, Foo> as (Bool, Foo), then this is a 1-1 mapping:

extension OneOf where First == Foo, Second == Foo {
    init(_ tuple: (Bool, Foo)) {
        if tuple.0 { self = .second(tuple.1) }
        else { self = .first(tuple.1) }
    }

    var asTuple: (Bool, Foo) {
        switch self {
        case .first(let f): return (false, f)
        case .second(let f): return (true, f)
        }
    }
}

I still don't get why a tuple is different. When I mentioned (Foo, Foo) as an alternative to Either<Foo, Foo>, I think the user would want to use something like this:

typealias Data = (componentA: Int, componentB: Int)
let data = (componentA: 5, componentB: 6)

But I fail to see how OneOf<Foo, Foo> would be useful - with the extension you wrote. Let's say I want to choose between a state where the user either passed a test and to show their score or they didn't with their mistakes:

typealias TestResult = Either<Score, Mistakes>

let passedTestResult: TestResult = .choosing(Score(10))
let failedTestResult: TestResult = .choosing([
     Mistake.wrongAnswer(5),
     Mistake.wrongAnswer(3)
])

Whereas when I have just scores, what's the point of using Either a not just the bare type Score:

typealias OneOfScores = Either<Score, Score>

let oneOfScores: TestResult = .first(Score(10))

Why not just use a value of type Score:

let score: Score = 10 //or 
                    9 // or whatever value

Your sum type Either<Score, Mistakes> provides an extra bit of information (pass/fail) by virtue of being a tagged union. The sum type Either<Score, Score> also provides that extra pass/fail bit. If you don't provide that bit, and you just give me a bare Score, how can I tell if I passed or failed?

Thanks for the explanation, I get it. It would be useful in the hypothetical OneOf type too. For example, (if labels are added as sugar - let’s not focus on the implementation part):

typealias MyOneOf = (
    passed: Void | 
    failed: Void | 
    wasAbsent: Void
)

One thing we should decide is whether this is worth the trade-offs. For example in the above context initializing OneOf with Void is ambiguous. So what would we do, .passed? Will such a feature be allowed in an unlabeled OneOf type, which will likely be the first to appear?

Still, though, in an Either<Foo, Foo> type would be of limited usefulness. If given the choice I think it’d be better if we gave an error in the same-type Either example, that would prompt the user to switch to a tuple (Foo, Bool).

As I have mentioned several times upthread, one of the primary places structural sums arise is in composition. When they arise under this condition, replacing the sum type with a tuple is not an option. The sum type representation is necessary in some circumstances.

Even when it isn’t, it would be a bad idea to prevent people from using the sum type representation. Nowhere else in the language do we arbitrarily prevent people from using an otherwise valid type.

4 Likes

It's not possible to give an error today (at least not a clear one, I'm not sure if there's something you could hack together to make the compiler reject it) because you would need negative type constraints: enum Either<First, Second> where First != Second. If you want this, now the very simple Either type you're advocating for starts requiring complex new features added to the type checker.

And even then, it's a bad idea to apply that restriction because you're only considering the case of someone writing literally Either<Foo, Foo> in their source code—which is still fine if someone wants to do it, IMO, because two values of the same types can have different semantics which is what the cases capture—but you could also end up with the same type through generic substitution and associated type resolution where it isn't obvious in the source code:

extension Collection {
  subscript(indexOrOffset indexOrOffset: Either<Index, Int>) -> Element {
    ...
  }
}

let array = ["foo", "bar", "baz"]
let element = array[indexOrOffset: /* some value */]
// ^^^ Array.Index == Int,
//     so this argument is Either<Int, Int> and it's perfectly valid

So either you'd have to arbitrarily ban the subscript above as well, or construct your Either type such that it forbids the types to be the same when they're written literally at the type declaration site but let people get around it using generics—neither of these makes sense.

9 Likes

I also would like to ask the community what conformances should be conditionally available and implicit setting and accessing of an Either value?

Basic

The protocols that are in the top of my head are:

  1. Equatable
  2. Hashable
  3. Encodable
  4. Decodable
  5. Error

Less Likely

There is also the possibility of adding ExpressibleBy...Literal . What are your thoughts on this? Should we provide:

setFontWeight(to: 600) // Does the user expect that Either
                       // will be used here?
setFontWeight(to: .choosing(.bold))

Much less likely

Or I guess, we could give Either an Optional like behaviour, by providing implicit casting:

setFontWeight(to: .bold) // allowed

and also explicit casting to a wrapped value:

let result: Either<Score, Mistakes> = ...
let score = result as? Score // allowed

The problem with this approach, among others, is that Either< Score, Score > would be possible:

let result: Either<Score, Score> = ...
let score = result as? Score // Error! But what exactly will the compiler do?
                             // I think it's very unlikely that an entirely
                             // new feature will be created as sugar 
                             // for the humble `Either` type

The same applies for initialising or setting an Either value. That problem would be unique to Either (as far as I know; if you know any other types that mimic that behaviour please share them). Neither AnyHashable nor Optional face this problem, since both have only one generic value.

Personally, I don't see how this could be easily fit in with the current casting mechanism, without providing new diagnostics etc. Again, I'm not really familiar with how it works in behind the scenes so that's just speculation. What I feel though would be a good alternative is simple methods - and computed properties:

setFontWeight(to: .choosing(.bold)) // set with `choosing`
let score: Score? = result.castedValue // access with `castedValue`

At first, I thought that a simple value property could do the job, but the user may not realise that information is lost in the new variable score - since it'll be unknown whether score is the first or the second value in our result type:

let score = result.value as? Score // more explicit
                                   // (here `value` is of type Any)

Thoughts?

There are valid reasons to use Either<String, String>, for instance. Consider a case where you want to create a string or explain why you couldn't create the string. Yes better type representations should be used and all of that but it is pretty trivial to think of cases where you want to represent two different ideas/cases with the same type and Either<A, A> allows you to do that and keep track of which case you have.

1 Like

Basically an Either<A, A> (or similar) type could have the following extension:

// extension<T> Either<T, T> {
extension Either where First == Second {
  var value: First {
    switch self {
    case .first(let value),
         .second(let value):
      return value
    }
  }
}

Of this could be a non-throwing get() method similar to Result.

However I do not advocate for the inclusion of Either into stdlib with this.

2 Likes

I assume by "with this" you meant the "non-throwing get() method similar to Result".

Along with your value property we could provide some more:

extension Either {
    @inlinable
    public var value: Any {
        switch self {
        case .first(let value): return value
        case .second(let value): return value
        }
    }
}

extension Either where Second == Never {
    @inlinable
    public var value: First {
        switch self {
        case .first(let value): return value
        }
    }
}

extension Either where First == Never {
    @inlinable
    public var value: Second {
        switch self {
        case .second(let value): return value
        }
    }
}

@filip-sakel, could you make a proposal out of this, I would be very interested.

Guys, could we exclude implicit conversion/coercion issues for the introduction of the Either type.

It is just not the right place, once we have them in sdtlib, we can talk about such issues, but not yet, make the first proposal thin and leave room for further extensions.

It demonstrates that people continue to need to be educated about why union types are not a fit for Swift

I concur with your opinion as I'm one of those still not understanding what the problem about implementing union types is except the point that it's hard to implement it in Swift.

But many things are hard to implement, and we do it anyway, it would be nice to get a bit more invited into the implementation issues.

Sorry for taking so long to reply.

There's some discussion currently going on in this thread. I am interested to see if the community reaches a consensus on a more general-purpose approach for an Either-like type. Now, though, I'm quite busy anyway.

I agree. I think that whatever consensus the community arrives at, it is important to keep in mind that such a type will probably not support conversion or coercion in its first version.

1 Like