Adding Either type to the Standard Library

Would the requirement to cast in some way before being able to use a value not count as explicit projection? (I might be mixing out projection/injection? Also I'm assuming wrapping a value in a union type implicitly would not result in major type-checker problems?)

Great ideašŸ˜ƒ. Having Either<First, Second> Type in Standard Library allows us to use the same type across libraries. Similar situation was with the Result<Success, Failure> Type.

Either type is very useful with UDF / Rx / Combine and other cases. Some examples:
let completion: (Result<Either<AuthAgreement, ActiveLogin>, Error>) -> Void
func eitherRequest<L, R>(_ target: Target, completion: @escaping NetworkCompletion<Either<L, R>>

let quizScoreViewModel: Either<ScoreViewModel, String>
let quizAnswer: Either<String, [QuizAnswerVariant]>
let showError: Either<ErrorVM, TextSheetVM>

Our implementation:

/// Either represents duality, a value that can either be of a type or another
public enum Either<L, R> {
  case left(L)
  case right(R)
}

extension Either {
  public var leftValue: L? {
    switch self {
    case .left(let leftValue): return leftValue
    case .right: return nil
    }
  }

  public var rightValue: R? {
    switch self {
    case .right(let rightValue): return rightValue
    case .left: return nil
    }
  }
}

extension Either where R: Error {
  public func asResult() -> Result<L, R> {
    switch self {
    case .left(let leftValue): return .success(leftValue)
    case .right(let rightValue): return .failure(rightValue)
    }
  }
}

extension Either where L: Error {
  public func asResult() -> Result<R, L> {
    switch self {
    case .left(let leftValue): return .failure(leftValue)
    case .right(let rightValue): return .success(rightValue)
    }
  }
}

extension Either: Equatable where L: Equatable, R: Equatable {}

extension Either: Hashable where L: Hashable, R: Hashable {}

extension Either: Decodable where L: Decodable, R: Decodable {
  public init(from decoder: Decoder) throws {
    do {
      let decodedInstance = try L(from: decoder)
      self = .left(decodedInstance)
    } catch let leftError {
      do {
        let decodedInstance = try R(from: decoder)
        self = .right(decodedInstance)
      } catch let rightError {
        let info = "Type: \(Self.self), \n left: \(String(reflecting: leftError))\nright: \(String(reflecting: rightError))"
        throw JsonMappingError(code: .typeCastFailed,
                               userInfo: ["EitherMappingFailed": info])
      }
    }
  }
}
1 Like

If we're going to push a simple Either type then I would prefer the first and second case names as they almost mirror the index based names of a potential variadic OneOf enum, except that they don't start with zero. Ultimately I still would prefer OneOf (but maybe with a different/better name) and would stop using Either immediately, even to represent duality.

2 Likes

To everyone discussing union types, subtyping and implicit conversions: please stop. The core team has made it abundantly clear that they do not believe this feature is a fit for Swift. They have provided very good reasons for this. Discussion in this direction is not helpful and distracts from discussion of directions that are viable and fill gaps in the language.

Please donā€™t mix people who continue trying to push union types with those of us who are focused on trying to discuss structural sum types. Yes, a lot of people are attracted to the idea of union types. But that is not an argument against structural sum types. It demonstrates that people continue to need to be educated about why union types are not a fit for Swift.

The problem with Either is that it leaves us with either clunky nesting or our own OneOf3, OneOf4, etc. Either is a narrow solution. Many of the use cases I run into are in cases of composition which are best designed as variadic. The language should provide a solution that works in this context.

Please letā€™s not. Letā€™s focus on creating examples that demonstrate why structural sum types are a better approach.

8 Likes

Actually, I haven't even seen a convincing argument not to add support for | at type levelā€¦

Although I agree that a dedicated, unspecific Either-type isn't a good addition, that is what the thread starter wants to discuss here ā€” and imho we should respect this request.

For a huge and complicated pitch, it might make sense start collecting reasons for dismissal upfront ā€” but for a simple thing like enum Either<First, Second>, imho it is fine to focus on improvements and wait with negative feedback until review.

2 Likes

as? could be used as the projection syntax, yes, and I believe that would not have particularly severe type-checking problems.

"Projection" was probably not the best word-choice here on my part; I just meant in general getting at the underlying values.

as? would still have the ambiguity problem when more than one tag in the sum has the same payload type, though.

5 Likes

I completely agree. This thread has been overwhelmed by discussions of counter-proposals.

Would the core team prefer the community to start separate threads every time there are different fundamental approaches to solving the same problem? We could certainly do that, but what would the advantages be? Keeping the discussion in a single thread gives participants with different views more opportunity to interact, which I think is a healthy thing.

I don't see a whole lot of interaction with the original proposal in here. It's 90% people discussing the language design of structural sum types. The OneOf I'm seeing described is essentially the same; it's not implementable without compiler magic because the cases somehow get different names.

Thank you for understanding, but I should clarify something: Iā€™m not against anything other than Either - after all thatā€™s what discussion is all about. Nonetheless, I appreciate implementable proposals.

The possibility of adding OneOf, OneOf2... is worth exploring, but we should also consider what would happen when adding a variadic OneOf. Source might not break, but ABI might - Iā€™m not really familiar with ABI stability so please correct me if Iā€™m wrong. Also will library authors be willing to switch to the variadic OneOf once it comes out, or will OneOf3 become a typealias if itā€™s variadic counterpart? I think these are valid questions worth exploring when discussing currently implementable designs.

Yes. In a generic context where types may be identical, it would create nasty particular cases:

struct Stuff<Left, Right> {
    var value: Left | Right

    var left: Left? { value as? Left }
    var right: Right? { value as? Right }
}

let stuff = Stuff<Int, String>(value: ...)
assert((stuff.left == nil) != (stuff.right == nil), "Easy, right?")

let stuff = Stuff<Int, Int>(value: ...)
assert((stuff.left == nil) != (stuff.right == nil), "Oh, well...")

Yes, you're absolutely right. I was analyzing it syntactically and not thinking about the semantics.

1 Like

In my opinion, tuples provide significantly more value than an Either struct would.

In addition to the common problems with tuples that Either would need new or breaking API when you move to a named type, Either would not have the option to provide semantic naming to the individual values of a particular usage.

Tuples do not provide encapsulation or association of logic - I cannot add methods to my (first: String, second: String) tuple like I can a struct. Either on the other hand would provide a mechanism for extensions to be used to attempt to carry forward the use of the Either type to library and application specific use cases.

In this way, I am concerned that the little bit of benefit of ease of use with either-vs-enum (in that it fixes a very narrow need of reducing typing when you quickly need a two option tagged union) encourages significantly more accumulation of tech debt in the resulting library than tuples-vs-structures as a comparative feature relationship.

3 Likes

Iā€™m assuming you are talking about Union Tuples - not regular ones.

I donā€™t think it would. Up-thread there has been a lot of discussion about an OneOf type. Or how multiple such types (OneOf2, OneOf3...) could be added. I think the ā€œmultiple such typesā€ post will give you an ideas of how we could migrate to a variadic OneOf. To sum it up, we would just deprecate Either and encourage the use of OneOf in its place. So if youā€™re concerned that Either will limit our future options, you shouldnā€™t.

Compiler magic to convert the expression:

typealias MyOneOf 
    = (foo: Foo | bar: Bar)

Could be converted to:

typealias MyOneOf(first|second|) 
    = OneOf<Foo, Bar>

That would be similar to another post that proposes baking the closure labels into the identifier. That though is a future proposal altogether, that requires to be built one top of OneOf. There has also been talk of a ā€œnativeā€ Union Tuple type, that will have custom compiler support. Nonetheless, that type is not acceptable to the Core Team, as they are hardly keen on the idea of an OneOf a type that is currently unimplementable as it requires advanced variadic genetics - correct me if Iā€™ve misinterpreted. All in all, I would appreciate ideas that are applicable to the proposal of adding an Either like type. Lastly, Iā€™d like to say that I donā€™t see a Union Type built into the compiler as a reasonable option, because it would further complicate the compiler and because there has been talk of even replacing Tuples with a normal type.

I donā€™t believe union tuple is a common term.

Iā€™m talking about the comparison between tuples and structs, vs either/anyof and enum.

The tuple (Double, Double) isnā€™t nearly as useful as a labeled (x:,y:), (r:, angle:), (width:, height:), etc. Either<String, Int> fails to allow for capturing what the semantics actually are - a string or int of what? When is the value one vs the other?

A tuple has clear limitations where it does not support encapsulation or polymorphism - you cannot have a tuple implement a protocol or add additional methods. You can do both of these with an Either type. This creates tension in that people can leverage an either/anyof type long after they should have moved to enum (perhaps because moving from either to enum is a breaking change). Examples of changes might be adding properties to give ā€˜friendly namesā€™ to left/right, implementing protocols like Comparable, etc.

3 Likes

I think @Dmitriy_Ignatyev lays out pretty compelling reasons for Either's addition:

I feel like a need to clarify that Either won't try to replace all 2-case enums and neither will OneOf replace all enums, just as Tuples haven't replaced structs. Tuples still have labels and they are more concise, still though structs are usually preferred. Either just as Tuples, fits naturally in places where the creation of a new type just doesn't make sense. For example, such a type is present in the Standard Library, in the "automatic protocol satisfaction" proposal, in function builders it would make sense if it was used instead of two separate - and confusing - buildEither methods. Additionally, in places where the creation of a new type is currently not allowed:

struct FooGeneric {
    func fooGeneric<Baz>(baz: Baz) {
        enum State { case bar(Bar), case foo(Foo) } // āŒ 

        ...
    }
}

of course we can do:

struct FooGeneric {
    enum State { case bar(Bar), case foo(Foo) }

    func fooGeneric<Baz>(baz: Baz) {
        ...
    }
}

but in the above cases if we use State just in fooGeneric it would make much more sense for an Either type to take State's place. Not to mention the boilerplate that would be required should we want Equatable conformance.

I recognise that Either is not the ideal type, we'd all like to have. Nonetheless, I feel that it is needed for the cases outlined above - or at least it would make them more pleasant to deal with. Finally, let's not forget that having an OneOf as I mentioned before, is not an option currently, so adding Either for now seems like a good option and discussing how to best implement is the goal of this discussion. If you have any other ideas that are realistic for the above examples and is more elegant than Either feel free to share them.

1 Like

One important thing to keep in mind is that there is really no adding something to the standard library "for now". If something is added to the standard library, then it will be there for a very long time unless the Swift team is willing to break source and/or binary-compatibility to remove it.

Thus, additions to the standard library should meet a very high bar, and any such proposal should explain why it is critical that it be added now instead of focusing those efforts on building up the lower-level foundations of the language like variadic generics and constructing some way of expressing variadic cases so that something better could be provided, sinceā€”by your own post aboveā€”"Either is not the ideal type".

7 Likes

This is why I am opposed to introducing Either and why I think it is appropriate to discuss things like structural sum types in this thread.

4 Likes

First of all, Iā€™d like to address the part regarding focusing our efforts on variadic generics. I think everyone wants variadic generics, especially me. Iā€™ve been working with function builders and without variadic generics some parts are really mess. But I hate to see, that discussion about variadic generics has been going on for years, yet still to this date thereā€™s no such feature. Not to mention that an OneOf type would require probably an advanced Variadics feature, as repetition of an element - OneOf<Foo, Foo> - should not be allowed.

What has been presented in this thread are real problems - in my previous post I think I mentioned some. So, seeing function builders, the standard library and other projects result to custom types and more repetitive code - that in most cases are just boilerplate - I though that a sensible direction would be a standardized Either type. And many counter-proposals I donā€™t see a viable alternative. What Either offers is a great set of features - custom conformance, a standardized type etc.

Yes I still think Either isnā€™t ideal overall, but itā€™s best for now, where there is a need for such a type. Furthermore, if a new Variadic type comes along and renders the use of a two-case Either type useless, it could slowly be deprecated and removed.

To sum up, there are problems that require an Either like type, whether itā€™ll be named Either or OneOf doesnā€™t matter, what matters is that those problems be solved. Once an OneOf comes along, which will most likely take a while, Either will have already helped in a lot of situations and eliminated a lot of boilerplate.