Home > front end >  How to differentiate what generic value is what in Swift?
How to differentiate what generic value is what in Swift?

Time:10-17

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 a Success or a Failure, and
  • a closure that takes a Failure and returns a Success

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.

  • Related