Calling a different init function when passing optional vs. non-optional values into a generic parameter

When passing a value into an init function with a generic parameter, you can pass both optional and non-optional values in via the parameter, but inside the function you are unable to differentiate between if you are dealing with an optional or not which can be problematic. I am looking for a way to selectively call one init function if you are passing in an optional and another init function if you are not using an optional. I have managed to solve this actually, however in my solution I get a compiler warning stating "Same-type requirement makes generic parameters 'T' and 'Value' equivalent; this is an error in Swift 6".

Please see code sample and follow up questions below:

import Foundation

struct A<Value> {
    
    init(value: Value) {
        print("A init:", value)
    }
}

struct B<Value> {
    
    // always called
    init(value: Value) {
        print("B init for non-optional value:", value)
    }
    
    // never will be called
    init<T>(value: Optional<T>) where Value == Optional<T> {
        print("B init for optional value:", value)
    }
}

struct C<Value> {
    
    // called as expected, but compiler gives warning 'Same-type requirement makes generic parameters 'T' and 'Value' equivalent; this is an error in Swift 6'
    init<T>(value: T) where Value == T {
        print("C init for non-optional value:", value)
    }
    
    // called as expected
    init<T>(value: Optional<T>) where Value == Optional<T> {
        print("C init for optional value:", value)
    }
}

let nonOptionalString: String = "1"
let optionalString: String? = nil

_ = A(value: nonOptionalString)     // prints 'A init: 1'
_ = A(value: optionalString)        // prints 'A init: nil'

_ = B(value: nonOptionalString)     // prints 'B init for non-optional value: 1'
_ = B(value: optionalString)        // prints 'B init for non-optional value: nil' <-- undesired behaviour

_ = C(value: nonOptionalString)     // prints 'C init for non-optional value: 1'
_ = C(value: optionalString)        // prints 'C init for optional value: nil'     <-- desired behaviour

Struct A demonstrates base case behaviour of one init method that you can pass both optional and non-optional values into. Within the init you are unable to treat the value as an optional even if an optional was passed in (this makes sense of course, but means you can never deal with the optionality of a passed in optional).

Struct B demonstrates adding an additional init method to handle the optional. This gives no compiler warning, but this additional init method never gets called.

Struct C demonstrates a working solution similar to struct B but where the the original init method is modified to use function generic type T and constrain T equal to Value. This solution works but gives a compiler warning related to upcoming changes in Swift 6.

Questions:

  1. Is there a better way to accomplish calling different init methods for optional vs non-optional values passed into a generic parameter?
  2. Why do the init methods in struct B and struct C differ in their behaviour? The compiler is correct when it says T and Value are equivalent, but T constrained equal to Value is actually treated differently than just using Value directly.

Thanks for your help!

I realize this is a somewhat unsatisfying response to get, but if you don't mind my asking—why are you trying to do this? A lot of the time it's an indication that there may be a better way to model your system. Having an ostensibly generic type change behavior based on the specific identity of the type underlying its generic parameter can be confusing for users of the type.

As an example, consider what would happen (even with the 'working' version) if you tried to construct C from a generic context:

func constructC<T>(using t: T) -> C<T> {
  C(value: t) // always uses the non-optional init!
}
constructC(using: optionalString)

Suddenly, all downstream clients need to be acutely aware of the "special" behavior for optional in order to work with your type C properly.

When two functions with the same name (such as two initializers) both 'match' at the use site, the compiler has some rules to decide which one is a better fit. For instance, if one function has type (A) -> Void and the other has type (B) -> Void where B is a subtype of A (e.g., B is a subclass of A), the compiler will pick the (B) -> Void function.

These disambiguation rules don't cover every case, and so sometimes you'll get 'ambiguous use of ___' errors that force you to specify which member you're talking about explicitly. But even when they do 'work', relying on them can make code difficult to reason about.

The rule that (I think) is kicking in here is that the compiler considers non-generic declarations to be more specific than generic declarations, so by turning the first init in B into a generic init in C, you have actually subtly altered the overload ranking and put the two inits on more 'equal' footing (for potentially further disambiguation rules to differentiate).

2 Likes

You don't need to make either init generic.

struct A<Value> {
    init(value: Value) {
        print("non-optional: \(value)")
    }
    
    init(value: Value?) {
        print("optional: \(value)")
    }
}

let non = "non"
let opt: Optional = "opt"

_ = A(value: non) // prints: non-optional: non
_ = A(value: opt) // prints: optional: Optional("opt")

Note that the use of Optional must be visible at the call to init. So if you construct A inside a generic function that doesn't know its parameter is Optional, you break things:

func indirect<V>(_ value: V) -> A<V> {
    return A(value: value)
}

_ = indirect(non) // prints: non-optional: non
_ = indirect(opt) // prints: non-optional: Optional("opt")

If you're going to construct A through a generic function, you'll need to provide two versions of that generic function also, one taking an Optional:

func indirect<V>(_ value: V) -> A<V> {
    return A(value: value)
}

func indirect<V>(_ value: V?) -> A<V> {
    return A(value: value)
}

_ = indirect(non) // prints: non-optional: non
_ = indirect(opt) // prints: optional: Optional("opt")
2 Likes

This achieves a subtly different result since with C above the identity of Value when passed a String? is String?, but in the init(value: Value?) case, the generic parameter takes on the type String. Of course, it's possible this is fine in context (and possibly even desirable!) but just wanted to note the difference in behavior.

Thanks for the info and help! The short story is that I'm working on an @UserDefault property wrapper. I know there are tonnes of examples of these online, but I'm working on one that works exactly how I want it to work. Due to how UserDefaults removes a value when nil is passed to set(_:forKey:), I end up with some odd behaviour when working with properties of optional types with a default value that's not nil.

Example:

@propertyWrapper
final class UserDefault<Value> {

  public init<T>(_ key: String, defaultValue: Optional<T>) where Value == Optional<T> {
    // special logic discussed later goes here if defaultValue is not nil
    // ...
  }

  public init<T>(_ key: String, defaultValue: T) where Value == T {
    // ...
  }

  // ...
}

@UserDefault(key: "a", defaultValue: 0) var a: Int
@UserDefault(key: "b", defaultValue: nil) var b: Int?
@UserDefault(key: "c", defaultValue: 0) var c: Int?   // <-- the problem case

print(a)            // prints '0' (default value)
a = 20; print (a)   // prints '20'

print(b)            // prints 'nil' (default value)
b = 20; print(b)    // prints '20'
b = nil; print(b)   // prints 'nil' b/c nil is the default value for b and the entry in UserDefaults was removed by set(_:forKey:)

print(c)            // prints '0' (default value)
c = 20; print(c)    // prints '20'
c = nil; print(c)   // prints '0' b/c 0 is the default for c and the entry in UserDefaults was removed by set(_:forKey:)

In case c, I want to be able to actually set nil as a value and have the variable return nil when you read from it instead of returning the default value. How I solve this is to have the property wrapper pass the string "<nil>" to set(_:forKey:) instead of passing an actual nil. I then also interpret receiving "<nil>" from UserDefaults as meaning to return a nil for the wrapped value.

I want this storage of "<nil>" to only occur in the case where the property is an optional and the default value is non-nil to avoid unnecessary occurences of "<nil>" in the related plist file. I can't do this with just init(_ key: String, defaultValue: Value) however, hence the desire for two inits. Everything does work smoothly using the init functions above, but I get that compiler warning for Swift 6, so I'm hoping there's another way to accomplish this.

Good points re: weirdness for downstream clients. I had not thought of that. I think the discussion I posted just now about the property wrapper should clarify that this shouldn't end up being an issue for my case, but the points you raised make a lot of sense for the broader context of the language.

Thanks for explaining the disambiguation rules. I figured it was something like that, but didn't know where to go to find more information. Do you know if those rules are documented anywhere?

Thanks for the help! As @Jumhyn pointed out though there is a slight difference with what the type of Value ends up being with that approach. In the context of the property wrapper that I'm working on (which I just posted as a follow up), I end up needing the type of Value to be exactly as it is in C (e.g., to be Optional<T>).

Here's an example of C compared to a new struct D (which uses the Value vs Value?) approach:

struct C<Value> {
    
    init<T>(value: T) where Value == T {
        print("C init for non-optional value:", type(of: Value.self))
    }
    
    init<T>(value: Optional<T>) where Value == Optional<T> {
        print("C init for optional value:", type(of: Value.self))
    }
}

struct D<Value> {
    init(value: Value) {
        print("D init for non-optional value:", type(of: Value.self))
    }
    
    init(value: Value?) {
        print("D init for optional value:", type(of: Value.self))
    }
}

let nonOptionalString: String = "1"
let optionalString: String? = nil

_ = C(value: nonOptionalString)     // prints 'C init for non-optional value: String.Type'
_ = C(value: optionalString)        // prints 'C init for optional value: Optional<String>.Type'

_ = D(value: nonOptionalString)     // prints 'D init for non-optional value: String.Type'
_ = D(value: optionalString)        // prints D init for optional value: String.Type''

That approach does resolve the issue of calling both inits correctly though which is a step above struct B from original post and doesn't have the compiler warning issue that struct C has, but the String vs Optional kills my use case.

Thanks for the discussion re: indirect example too. I had encountered that in some other playing around with things I was doing, but that makes it more clear what's happening.

Found a solution that does what I want. Behold struct E:

struct E<Value, T> {
    init(value: Value) where Value == T {
        print("E init for non-optional value:", type(of: Value.self))
    }
    
    init(value: Optional<T>) where Value == Optional<T> {
        print("E init for optional value:", type(of: Value.self))
    }
}

let nonOptionalString: String = "1"
let optionalString: String? = nil

_ = E(value: nonOptionalString)     // prints 'E init for non-optional value: String.Type'
_ = E(value: optionalString)        // prints 'E init for optional value: Optional<String>.Type'

Thanks again @Jumhyn and @mayoff! @Jumhyn your point about the disambiguation rules tipped me off that both inits have to either both have a generic parameter or both not have one and @mayoff your point about not needing to make either init generic, tipped me off that I could not make them generic and instead move the generic into the type itself. T ends up being a bit redundant in the case of the first init, but hey my tests pass and the compiler is happy, so I'm happy!

1 Like

Got it, thanks for explaining what you're trying to do. How would the scheme you've devised handle a situation like this?

@UserDefault(key: "c", defaultValue: "") var c: String?

print(c)
c = "<nil>"; print(c)    
c = nil; print(c)

?

In general you're right that the UserDefaults API makes it somewhat problematic to distinguish between the "never set" and "explicitly set to nil" cases. Trying to represent the "explicitly set to nil" case in the 'space' of the underlying value, though, will require you to 'burn' one of the valid values to use as a sentinel for nil. Maybe that's okay for your use case, and you'll never set anything to nil.

But if you'd like to avoid that, you'll need to have a way for your "never set," "set to nil," and "set to non-nil value" representations to all be distinct. You could accomplish this by, for example, defining a protocol for use by your UserDefault type, e.g., UserDefaultStorable that has a couple requirements like:

func storeInUserDefaults(for key: String)
func extractFromUserDefaults(for key: String) -> Self?

The types that are directly representable (Int, String, etc.) could just forward to the corresponding methods on UserDefaults, and then Optional could have a conditional conformance to UserDefaultStorable whenever the wrapped type conforms to, say, Codable. You could encode the wrapped value to a JSON String if present and then encode .some(value) yourself as { "value": <value string> } and .none as {}, then store that string in the user defaults. When fetching from user defaults, you'd reverse this process. If the user default returns nil that means you never set any value and you should return nil (i.e. Optional<Optional<T>>.none) from the extractFromUserDedaults method. But if there is a String, then you can decode the Optional<T> value to either Optional<T>.nil or Optional<T>.some(...), and return that from the extract method (i.e., return Optional<Optional<T>>.some(Optional<T>.none) or Optional<Optional<T>>.some(Optional<T>.some(...)) respectively.

This extra layer of optionality is what allows you to keep the representations of "never set" and "explicitly set to nil" distinct, and then your UserDefaults wrapper can be agnostic to the underlying identity of the wrapped type. All it needs to know is that the type itself is able to produce a value when it has been set, and return nil if it hasn't been set.

2 Likes

That is a very interesting and smart approach. I will have to play around with that idea. Thanks again!