Generic function that requires the generic type to be non-optional

For example:

struct Foo {
    typealias WritableKeyPath<Value> = Swift.WritableKeyPath<Foo, Value>
    var optional: Int?
    var nonOptional: Int = 0

    mutating func set<Value>(keyPath: WritableKeyPath<Value>, value: Value) {
        self[keyPath: keyPath] = value
    }
    mutating func set<Value>(keyPath: WritableKeyPath<Value?>, value: Value?, onNilOnly: Bool) {
        if self[keyPath: keyPath] == nil || onNilOnly == false {
            self[keyPath: keyPath] = value
        }
    }
}

var foo = Foo()
foo.set(keyPath: \.nonOptional, value: 1) /// foo.nonOptional: 0 -> 1
foo.set(keyPath: \.optional, value: 10, onNilOnly: true) /// foo.optional: nil -> Optional(10)
foo.set(keyPath: \.optional, value: 100, onNilOnly: true) /// foo.optional: Optional(10) <- Does not change
foo.set(keyPath: \.optional, value: 100, onNilOnly: false) /// foo.optional: Optional(10) -> Optional(100)

/// I don't want follwing code to be compiled:
foo.set(keyPath: \.optional, value: 1000) /// foo.optional: Optional(100) -> Optional(1000)

What I want to achieve is, when setting an optional key path, the parameter onNilOnly must be passed in explicitly.

So apart from changing the function name for the Optional one, is there a way to require the first function's generic type to be Non-Optional? Something like:

mutating func set<Value>(keyPath: WritableKeyPath<Value>, value: Value) where Value != Optional { ... }

or

mutating func set<Value?>(keyPath: WritableKeyPath<Value>, value: Value)

or

mutating func set<Optional<Value>>(keyPath: WritableKeyPath<Value>, value: Value)

Swift type system doesn’t have negation logic. Perhaps that’s for the best.

The best I could think of (but haven’t tested), is to provide 2 overloads, optional one, and non-optional one, and have the optional version emit some error, like deprecation.

1 Like

Yes, I thought of that too with 2 overloads. But that leads to another inconsistent behavior between non-generic function and generic function. Let me explain:

Say we have 2 strings, one optional, one non-optional:

let string: String = "Non-Optional String"
let optionalString: String? = "Optional String"

And a normal function func foo1:

func foo1(string: String) {
    print("Non-Optional String: \(string)")
}

Naturally, you can only pass string to foo1, and an error will prompt if you pass optionalString to it:

foo1(string: string) 
// prints the expected value

foo1(string: optionalString) 
// error: Value of optional type 'String?' must be unwrapped to a value of type 'String'

However, we can easily have another function foo2 to accept an Optional<String>:

func foo2(string: String?) {
    print("Optional String: \(string as Any)")
}

Apparently we can pass both to this function:

foo2(string: string) 
// prints the expected value

foo2(string: optionalString) 
// prints the unwrapped value or nil

So we can come up with a conclusion for a normal function that takes an Optional parameter: unlike other non-generic functions only accept a parameter of the type it requires, it accept 2 types, the Optional type and the type it wraps.

However for generic functions, this logic does not exist:

func bar1<Value>(value: Value) {
    print("value: \(value)")
}

func bar2<Value>(value: Value?) {
    print("value: \(value as Any)")
}

bar1(value: string) // correct
bar2(value: string) // correct, same as previous

bar1(value: optionalString) // correct
bar2(value: optionalString) // correct, same as previous

In the examples above, function bar1 and bar2 are the same thing with a different (or is it?) implementation.

So it is clear for us to see, when it comes to generic type, Value and Value? represent the same thing, which is any type including Optional. And I can understand that, the Optional is just an another type after all, it only makes sense to make it a generic target type. But humbly I have to ask, is it correct?

First, it brings redundancy and ambiguity as the example given above shows where function bar1 and bar2 are the same expression.

Second, it breaks the consistency that we have learnt and got used to from the normal function, which is, when we see a parameter has a type without a "?" suffix, we know the parameter has to be unwrapped before being passed to the function; on the other hand, when we do see the "?" suffix, we know it accept 2 types, the Optional type and the type it wraps.

Finally, it omits the possibility to perform the delicate control over the generic type requirement. For a normal function, we can easily require the parameter to be non-optional, but we can not do such thing for generic function, because a generic type almost always include Optional.

It is not quite so. foo2 still take String? in both scenarios. What happened is that string is implicitly casted to String?, then passed as an argument. The distinction is subtle, but become more important in the next part.

You're right that Optional is just another type, and so can be Value.
The thing is Value and Value? doesn't represent the same thing.
If you try to print out the Value.self you'll see.

let string = "Non-Optional"
let optionalString: String? = "Optional"

func bar1<Value>(_ value: Value) {
    print("\(Value.self) with value \(value)")
}

func bar2<Value>(_ value: Value?) {
    print("\(Value.self) with value \(value as Any)")
}

bar1(string) // String
bar1(optionalString) // Optional<String>

bar2(string) // String
bar2(optionalString) // String

So bar1 takes the respective type, as expected.
What's interesting is bar2 that in both cases Value is String.
So the specialized signature of bar, in both cases, is bar2(_ value: String?), and again, string is casted to String?.

That is why, with some knowledge of type checking, we could use it to our advantage.
In the simplest form, the type checker minimise the implicit casting (non-optional -> optional among others).
Now if you have

func bar<Value>(_ value: Value) {
  print("Non-optional")
}

@available(swift, deprecated: 0.0, message: "Use non-optional version instead")
func bar<Value>(_ value: Value?) {
  print("Optional")
}

bar(string)
bar(optionalString)

The optionalString case is simple, it can't use the top one.
The string case is more interesting.
If the compiler decide to use bottom bar, it'll have to do 1 implicit conversion (string to String?).
If the compiler decide to use the top one, there will be no conversion.
So the compiler will always choose the top bar for non-optional variable.

It's probably not ideal for some use case. Though I don't think negation constraint will come any time soon. Negation in type system is a rather complex problem.

2 Likes

Wow, I did not see this.

I agree what you said in the previous thread that is for the best. And what I am feeling now is not the need for a negation constraint (maybe I did when I posted the topic...), but a sense of loss of the absence of a level of strictness. Taking file access permission as an example, you can either have read permission or read and write permission. The lacking of the read-only is what now I feel wired about.

Thanks a lot for the detailed walkthrough and explanation!

Another design angle is to have

  • setIfNil(optional:) vs set(nonOptional:) instead of
  • set(optional:ifNil:) vs set(nonOptional:).
1 Like

I have updated my API as per @Lantua 's suggestion, and it works as intended now.

But I believe there is still a bug in Swift:

func bar<T>(_ v: T) {
    if v == nil {
        print("\(v) is nil.")
    } else {
        print("\(v) is NOT nil.")
    }
}

let nilInt: Int? = nil
bar(nilInt)

I think the if block got optimized away and else block got executed directly, because it prints:

nil is NOT nil.

Update:

A version without the warning of:

Comparing non-optional value of type 'T' to 'nil' always returns false

func bar<T>(_ v: T) {
    if (v as Any?) == nil {
        print("\(v) is nil.")
    } else {
        print("\(v) is NOT nil.")
    }
}

let nilInt: Int? = nil
bar(nilInt)

Still prints:

nil is NOT nil.

Your example works correctly and as intended.

The static type of v is T, which has no constraints. In particular, T is not Optional, T is not ExpressibleByNilLiteral, and T is not Equatable.

The only way for the compiler to make sense of v == nil is to promote v from T to T?. It then uses the special overload of == that compares an optional of non-Equatable type to the nil literal (specifically, the right-hand side is _OptionalNilComparisonType).

However, since v was promoted from T to T?, it follows that the resulting T? is not nil—it cannot be nil, because it is exactly T?.some(v).

When you call your function with nil as Int? (aka Int?.none), the compiler understands that T is Int?. That means T? is Int??, and so the == operator ends up comparing Int??.some(v) with nil as _OptionalNilComparisonType.

And that comparison always returns false, because, again, Optional(v) is definitely not nil.

1 Like

It looks like this:

let v: Int? = nil
let promoted: Int?? = .some(v)
print(promoted == nil) // false

Implicit conversion can be surprising if you're not careful. In this case, you'll see a warning

Comparing non-optional value of type 'T' to 'nil' always returns false

Which is as described by @Nevin.

2 Likes

Also, if you instead do T?, you'll get what you expected

func bar<T>(_ v: T?) {
    if v == nil {
        print("\(v) is nil.")
    } else {
        print("\(v) is NOT nil.")
    }
}

let nilInt: Int? = nil
bar(nilInt) // nil is nil

This is because you pushed the promotion to the bar call site. So if v is already optional, there will be no promotion.

1 Like

Reply to @Nevin and @Lantua

So the v == nil will silently convert T to T?. This is easy to overlook. I guess casting T to Any and then perform the case checking should avoid the silent cast from T to T?:

func bar<T>(_ v: T) {
    if case Optional<Any>.none = v as Any {
        print("\(v) is nil.")
    } else {
        print("\(v) is NOT nil.")
    }
}

let nilInt: Int? = nil
bar(nilInt)

It now prints:

nil is nil.

(or the one from this thread)
Thanks guys.

1 Like

I will note that there is something funky going on, because replacing Optional<Any>.none with Any?.none in your code gives an error.

Thus:

func foo(_ x: Any) {
  switch x {
  case Optional<Any>.none    : print("Optional<Any>.none")
  case .none as Any? /* 1 */ : print(".none as Any?")
  case nil as Any?   /* 2 */ : print("nil as Any?")
  case Any?.none     /* 3 */ : print("Any?.none")
  default                    : print("default")
  }
}

// 1 & 2 both give warning: Case is already handled by previous patterns; consider removing it
// 3 gives error: Expression pattern of type 'Any?' cannot match values of type 'Any'