Problem accessing nested type defined in an extension to a generic type

Hi, the subject line sounds complicated, but my problem is simply this:

I wanted to use an extension to Optional suggested inside this blog post.

In addition to the func „or“ from the blog post I wanted to extend Optional with a nested type „UnwrapError“.

extension Optional {
    public struct UnwrapError: Error {
    }
    
    public func or(throw exception: Error) throws -> Wrapped {
        guard let unwrapped = self else { throw exception }
        return unwrapped
    }
}

var a: Int?

print(try a.or(throw: Optional.UnwrapError()))

Accessing this „UnwrapError“ does not work, I always get the following error:

Argument type 'Optional<_>.UnwrapError' does not conform to expected type 'Error'

Sure, writing Optional<Int>.UnwrapError() instead of Optional.UnwrapError() works but that can't be the only solution, right?

The error is somewhat confusing. The problem is that nothing about the type signature of your Optional.or method constrains the type parameter of Optional.UnwrapError.

Remember that nested types capture generic parameters from the outer type, so Optional<Int>.UnwrapError and Optional<String>.UnwrapError are different types. If this is not desired behavior, you can't use a nested type.

Otherwise, you can also define another overload of or. A version taking an Optional<Wrapped>.UnwrapError would allow the caller to write Optional.UnwrapError(), and the type parameter would be inferred. But again, if the type parameter has no semantic meaning here, a nested type might not be the right solution.

1 Like

Thanks, I didn't know that. Apart from using the Standard Library I haven't used generics much.

The following works:

public func or(throw exception: Optional<Wrapped>.UnwrapError) throws -> Wrapped {
        guard let unwrapped = self else { throw exception }
        return unwrapped
    }

Though this complicates the method signature, everything else works as expected and on the calling side there is no excess verbosity. Are there any downsides to this approach?

In a method signature, you can omit the generic parameters and it will infer them from the current scope. So Optional.UnwrapError means Optional<Wrapped>.UnwrapError. I don't really recommend using this technique, but it can help sometimes. @Douglas_Gregor hates it :slight_smile:

Well, the obvious one is you cannot pass an Error other than Optional<Wrapped>.UnwrapError, which makes me wonder why you need to pass the error in the first place. UnwrapError carries no state, so you may as well construct it inside your method.

Perhaps you want to use a default argument, like so:

public func or(throw exception: Error = Optional.UnwrapError())

Right... :-) My error, that would make no sense.

Using a default argument has two downsides:

  1. It would not read very well on the calling side, because all that remains would be try anOptional.or(). This could be solved with clever renaming ;-)

  2. public func or(throw exception: Error = Optional.UnwrapError()) now the compiler complains about Initializer 'init()' is internal and cannot be referenced from a default argument value

Imagine you didn't have a default argument here. Then the user of the library would need to construct an UnwrapError themselves, which they would not be able to do, because its initializer is internal! Either you need to define a public init() {} inside the error struct, or perhaps the or() method itself should not be public.

Thanks again for the explanation.

The or() method expects a general Error. You could call it using an other Error type as argument. So the compiler's interpretation is very strict or do I miss something?

I'm not sure I understand the question. But yes, or will accept any instance of a type conforming to Error as an argument.

let a: Int? = 5

class Foo: Error {}

print(try a.or(throw: Foo()))

But since default struct initializers are internal, you won't be able to initialize an Optional.UnwrapError instance outside your module (to pass it as an argument). If you are still wondering why the compiler complains about exception: Error = Optional.UnwrapError(), that is because the default value (Optional.UnwrapError()) will have to be initialized when you call the function without arguments (But it can't be initialized outside the module, because the default initializer of Optional.UnwrapError is internal. In other words, the compiler is telling you that you will not be able to a.or() outside the module. (Providing a default argument guarantees that you can).