Generally speaking (not saying this is what you suggested), I was wondering if the Result
type should be a protocol Either
so users could create domain specific result types and the standard libary provides an out-of-the-box concrete type Result
.
It was a shocker of a thought experiment and no amount of showering helps me come clean. For a start, naming was hard because I found I was basically modelling a logical OR. First barf was something like the following:
protocol Either {
associatedtype TrueValueType
associatedtype FalseValueType
func getTrueValue() throws -> TrueValueType
}
enum Result<ValueType>: Either {
typealias TrueValueType = ValueType
typealias FalseValueType = Error
case success(TrueValueType)
case failure(FalseValueType)
func getTrueValue() throws -> ValueType {
switch self {
case .success(let value): return value
case .failure(let error): throw error
}
}
}
Either
suggests exclusitivity but nothing prevents a concrete type from using the same type non-exclusively. This applies to the Result
type, too, whose stronger semantics make Result<Error, Error>
incoherent (this is different from Result<Never, Error>
).
The madness continued by putting naming aside. Could a user match these exclusivity semantics in a domain we're familiar with and implement it with a specific concrete type? I considered URLSessionTask
. A task fails or succeeds but never both. Would the API benefit from a domain-specific concrete result type which always included a urlResponse
? The only thing I could think of were these shockers:
struct URLSessionTaskResult: Either {
typealias TrueValueType = Data
typealias FalseValueType = Error
public var urlResponse: URLResponse?
func getTrueValue() throws -> Data {
return underlyingResult.getTrueValue()
}
private var underlyingResult: Result<TrueValueType>
}
extension URLSession {
func dataTask(with url: URL, completionHandler: @escaping (URLSessionTaskResult) -> ()) -> URLSessionTask {
dataTask(with: url) { (data, response, error) in
let result: Result
if let data = data {
result = .success(data)
} else if let error = error {
result = .failure(error)
} else {
fatalError() // ???
}
let taskResult = URLSessionDataTaskResult(urlResponse: response, underlyingResult: result)
completionHandler(taskResult)
}
}
}
But this quickly turned to a dumpster of fire (which would be offensive to dumpsters already on fire). In the above, it composed the concrete implementation. Sure, it doesn't have to, but the implementation of concrete type without it turns awkward or ugly. For example:
enum AnotherURLSessionTaskResult: Either {
struct Storage<ValueType> {
var value: ValueType
var urlResponse: URLResponse
}
typealias TrueValueType = Storage<Data>
typealias FalseValueType = Storage<Error>
case success(TrueValueType)
case failure(FalseValueType)
func getTrueValue() throws -> ValueType {
switch self {
case .success(let storage): return storage.value
case .failure(let storage): throw storage.value
}
}
public var urlResponse: URLResponse {
switch self {
case .success(let storage): return storage.urlResponse
case .failure(let storage): return storage.urlResponse
}
}
}
or
struct YetAnotherURLSessionTaskResult: Either {
typealias TrueValueType = Data
typealias FalseValueType = Error
public var urlResponse: URLResponse?
var value: TrueValueType?
var failure: FalseValueType?
func getTrueValue() throws -> Data {
switch (value, failure) {
case (.some(let value), nil):
return value
case (nil, .some(let error)):
return error
case (nil, nil):
fatalError()
case (.some, .some):
fatalError()
}
}
}
It added too much complexity and left too much room for programmer error. If you've stil kept your food down, this is all a long way of saying two things.
First, what, if anything, should stop people from writing incoherent things like Result<T, T>
for the same T
?
Second, it's not clear what having such a general purpose naming actually adds to the developer's toolkit when you can augment the semantics of the Result
type with domain specific types by composition.
Kiel