Did the world need another Swift Either type? No.
Are We suffering from Not-Invented-Here Syndrome? Maybe.
Did We still think this was a good idea? Definitely.
Either is a concept in many functional and strongly-typed languages which allows a value of either one type or another, to be stored in one field:
/// A response for the population query
struct PopulationResponse {
/// The list of people in the population
///
/// - Note: In 1.x, this was a list of names as `String`s.
/// In 2.x and newer, this is a map of UUIDs to `Person` objects
let people: Either<[String], [UUID: Person]>
}This implementation brings a few advantages:
This automatically conforms an instance of Either to various protocols if its Left and Right types also conform to them.
Currently, these are supported:
-
Equatable– Brings==and!=. WhenLeftandRightare unequal types, this considersleftto never equalright. When those types are equal, this ignores theleftness andrightness positions -
Comparable– Brings<,<=,>=, and>. When the positions are unequal types, this considersleftto never be less than nor greater thanright. When those types are equal, this ignores the positions -
Hashable– Allows an instance ofEitherto transparently be given the same hash as whatever value it contains -
CustomStringConvertible– Provides a.descriptionfield with the same value as theEither's contained value's.descriptionfield -
CustomDebugStringConvertible– Provides a.debugDescriptionfield with the same value as theEither's contained value's.debugDescriptionfield -
Codable– AllowsEitherinstances to be encoded. This results in a multi-value keyed container which only ever contains one key-value pair where the key is"left"or"right", and the value is whatever the instance's value encodes to:{ "either": { "left": { "name": "Dax", "favoriteColor": 6765239 } } }or:
{ "either": { "right": 42 } } -
SendableSimply declaresSendableconformance whenLeftandRightare alsoSendable
Obviously you gotta eventually get a value out of this, and it offers a few approaches:
left– If theEitheris a.left, then that value is returned, elsenilright– If theEitheris a.right, then that value is returned, elsenil
When both Left and Right are the same type, then these are also available:
value– The current value, disregarding whether that value is.leftor.right*– Inspired by the semantics of dereferencing a pointer in C (and because Swift doesn't allow custom postfix!), place this before theEitherinstance for the same behavior as calling `.value:func name(_ user: Either<Person, Person>) -> String { return (*user).name }
Value– Since both positions are the same type, this typealias allows you to reference that type without specifically usingLeftorRight:typealias LegacyOrMigratedUser = Either<User, User> func account(of user: LegacyOrMigratedUser) -> LegacyOrMigratedUser.Value.Account { (*user).account }
This provides various approaches for mapping an Either. Generally these consider it a collection of exactly one element, similarly to how Optional is treated as a collection of exactly 0 or 1 elements.
-
map(left:right:)— Map both positions of thiseitherto different values/types, regardless of its current value. Only one of these callbacks is called each time this function is called (the one mapping a value), but this allows you to reuse the same call many times to map both sides depending on which one is set. -
map(left:)– Map only theLeftposition of thiseitherto a different value/type. The callback is only called when thiseitheris a.left -
map(right:)– Map only theRightposition. Inverse ofmap(left:)
This allows you to convert instances of some types into Either and back:
-
Optional– AnyEitherwhoseLeftisVoidcan be turned into anOptional<Right>, and vice versa anyOptionalcan be turned into anEither<Void, Wrapped>. Just pass one to the initializer of the other:let either = Either<Void, String>.right("I'm valued") let optional = Optional(either) print(optional!) // Prints `I'm valued`
var optional: String? = nil var either = Either<Void, _>(optional) print(either) // Prints `left()` optional = "I'm not sorry" either = .init(optional) print(either) // Prints `right("I\'m not sorry")`
-
Result– WhenEither'sRigthis anError, you can convert it to and from aResultsimilarly to the aboveOptionalconversions:let either = Either<Data, Error>.left(Data(base64Encoded: "SG93ZHk=")!) let result = Result(either) print(result) // Prints `success(5 bytes)`
var result = Result<Data, Error>(catching: { try Data(contentsOf: URL(string: "https://example.com")!) }) var either = Either<_, Error>(result) print(either) // Prints `left(1256 bytes)` result = .init(catching: { try Data(contentsOf: URL(string: "https://fakeDomain.fakeTld")!) }) either = .init(result) print(either) // Prints `right(Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened." UserInfo={NSURL=https://fakeDomain.fakeTld})`