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.
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:
- A Tuple -
(Foo, Foo)
or justFoo
- would be the correct way to do that. - Features like "
myType as? A
", "MyType(choosing: A())
" would not be able to be added in the future without source-breaking changes. - 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 = {}
Albeit not widely, it is used in the Standard Library as an internal type :
internal enum _Either<Left, Right> { case left(Left), right(Right) }
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?
(Foo, Foo)
is not the correct way to model a tagged union ofFoo
andFoo
.(Bool, Foo)
can model it.
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)
}
}
}
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).
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.
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
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.
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:
Equatable
Hashable
Encodable
Decodable
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.
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.
However I do not advocate for the inclusion of
Either
into stdlib with this
I assume by "with this" you meant the "non-throwing get()
method similar to Result
".
extension Either where First == Second { var value: First { switch self { case .first(let value), .second(let value): return value } } }
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.
@filip-sakel, could you make a proposal out of this, I would be very interested.
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.
could we exclude implicit conversion/coercion issues for the introduction of the
Either
type.
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.