Releases: RougeWare/Swift-Either
Release list
2.0 • Raw value decoding
Breaking Changes
While this update preserves existing decoding paths while adding an additional path, it does remove an error which might have previously been thrown.
In case any users of this library depended on that error to validate encoded data which doesn't match their desired format, this change is considered breaking and requires a MAJOR version bump.
Summary
Now Either can be used to decode payloads which encode different types in the same field, without wrapping those fields in "left"/"right" sub-objects.
The decoder now attempts to interpret a single‑value container as the left type, and if that fails, the right type. Of course, if both fail to decode, then a NeitherLeftNorRightValueWasEncoded error is thrown just as always.
Either<String, Int>{ "value": "foo" } // decodes to either.left("foo")
{ "value": 42 } // decodes to either.right(42)Usage notes
Since this only depends on which types the field can encode as, this will always decode as .left when the left and right types of the Either are the same:
Either<String, String>{ "value": "foo" } // decodes to either.left("foo")
{ "value": "baz" } // decodes to either.left("baz")1.1 • `Sendable` autoconformance
This simple change makes Either declare conformance to Sendable when both Left and Right conform to Sendable.
It ain't much, but it means a lot if you want to use this across concurrency barriers
1.0 • MVP
Yet Another Either Type! 🥳
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:
Automatic Conformance
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 } }
Unwrapping
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 }
Mapping
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:)
Conversions
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})`
1.0.1– Retroactive application of the orignally-intended and accidentally-omitted MIT license