How can I differentiate what generic value is for what in Swift?
For example, what does the value 'T' do and what does the value 'E' do?
func ??<T, E>(result: Result<T, E>, handleError: (E) -> T) -> T {
switch result {
case let .success(value):
return value
case let .failure(error):
return handleError(error)
}
}
CodePudding user response:
what does the value 'T' do and what does the value 'E' do?
They're not "values", they're the names of types — on a par with a term like String or Int. In fact, T could be String or Int. But E has to be some type of Error.
So the phrase <T, E>
, which appears twice, just refers to the fact that T and E are generic placeholders for their real types. When someone actually calls this ??
function, the caller will make clear what T and E really are. This is called resolving the generic.
So let's imagine that we call ??
in such a way as to resolve T to String and E to Error. Then in the mind of the compiler, we'll have this:
func ??(result: Result<String, Error>, handleError: (Error) -> String) -> String {
switch result {
case let .success(value):
return value
case let .failure(error):
return handleError(error)
}
}
So now we can read the function declaration. It says: "You hand me two parameters. One, result:
, must be a Result enum whose success type is String and whose failure type is Error. The other, handleError:
, must be a function that takes an Error and returns a String. And I will return a String to you."
Except, of course, that that is only one way out of an infinite number of ways to resolve T and E. They stand in for the real types that will be resolved a compiled time, depending on how ??
is actually called. So there's your answer; that is what they do. They are placeholders standing for the real types that will be resolved at compile time.
And in fact, to demonstrate, I will call your function in a way that resolves T to String and E to Error (though I will rename your function myFunc
to make the name legal):
func myFunc<T, E>(result: Result<T, E>, handleError: (E) -> T) -> T {
switch result {
case let .success(value):
return value
case let .failure(error):
return handleError(error)
}
}
enum MyError : Error { case oops }
let r = Result<String, Error> { throw MyError.oops }
let output = myFunc(result:r) { err in "Ooops" }
print(output) // Ooops
Footnote: Note that your function cannot really be called ??
, as that name is taken. It might have been better to call it foo
(the usual nonsense name in these situations).
CodePudding user response:
@matt's answer nails it, so I thought I'd add a like bit of higher level commentary.
Type parameter naming
The use of single characters for these generic type parameters is bit of a legacy a convention from Java and C#, but it need not be so concise. In this example, if you look at the main constraint on the types (Result
), note that they use Success
and Failure
. Using these here would provide a clearer idea about the intent of this function:
func ??<Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success
Thus, a function that takes:
- a
Result
that can either contain aSuccess
or aFailure
, and - a closure that takes a
Failure
and returns aSuccess
and returns:
- a
Success
Implementations bourne out of type system constraints
Note that as none of these types are wrapped in Optional
the implementation of this function is almost entirely constrained (side-effects notwithstanding).
Take, for example, the simplest function that could seemingly match:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
return Success()
}
Attempting to compile this gives the following error:
main.swift:2:12: error: type 'Success' has no member 'init'
return Success()
^~~~~~~
due to the fact that the Success
type is entirely unconstrained in Result
, so we don't actually know how to create one inside the function.
We know that Result
can contain a Success
, so what if we try and forcibly get that?
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
return result.get()
}
This now fails with the following compiler error:
main.swift:2:12: error: call can throw, but it is not marked with 'try' and the error is not handled
return result.get()
^
due to the fact that this function has explicitly denoted that it will not throw, and Result.get()
will throw the contained Failure
if it does not contain a Success
.
The other way to get the Success
out of a Result
is pattern matching, let's see how that works out matching the enumeration case pattern for a single if
:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
if case let .success(success) = result {
return success
}
}
When attempting this, the compilation error is (quite sensibly):
main.swift:5:1: error: missing return in a function expected to return 'Success'
}
^
So we need to also handle the case when a Result
contains a Failure
. Let's try with another pattern match, using the provided handleError
closure:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
if case let .success(success) = result {
return success
}
if case let .failure(failure) = result {
return handleError(failure)
}
}
This still gives the same error as the previous attempt, as the logic could still fall through in this case (this seems unlikely, but this could be subject to a kind of time-of-check to time-of-use bug).
Let's attempt it again, but matching both patterns in a switch
:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
switch result {
case let .success(success):
return success
case let .failure(failure):
return handleError(failure)
}
}
There we go, compiling without error.
As is obvious, this is the original function, as provided in your question.
Now that we've arrived back here, can we trim down this implementation? We could try something like:
func ?? <Success, Failure>(result: Result<Success, Failure>, handleError: (Failure) -> Success) -> Success {
switch result {
case let .success(success):
return success
}
}
but this provides this compilation error:
main.swift:2:5: error: switch must be exhaustive
switch result {
^
main.swift:2:5: note: add missing case: '.failure(_)'
switch result {
^
which indicates that we need that .failure(_)
case to match all possible outcomes of the result.