Inverse Conformance - Inequality Constraints

I was making a library that had code like the following:

protocol Foo {
    associatedtype Result

    ...
}   

And I wanted to do:

extensions Foo where Result != Never {
     func subscribe(forResult: Subscriber) {
         ... 
     }
}

I assume there has been previous discussion about such a feature, but I’m wondering the reasons why it wasn’t adopted. I want to discuss the addition of such a feature, as I think it’s really readable (Extension of Foo where the Result Type isn’t Never) and it also makes sense that some types, such as Never in the example above, don’t get some features.

4 Likes

It is often also advantageous to special case Void (and not Void). However, it's not possible and won't be anytime soon, and probably ever. It's been discussed a couple of times, and I think there's some kind of type theory reasons it can't be done, or maybe some practical reasons it would scale incredibly badly on the type checker or whatever. I'm sure other people (or a search) can give some answers. But the bottom line is that you have to make do without.

2 Likes

You shouldn't normally worry about special-casing Never. If a generic method requires values of a certain associated type, and that associated type is Never, then the method is already unavailable because it is impossible to call in the first place. Negative constraints become awkward when you want to invoke them from other generic contexts, because you would have to propagate the != Never constraint onto all of the generic arguments that eventually call into the context with the constraint, or else you wouldn't be able to call into it. It's generally better to design your code so that the right thing falls out of the normal type system rules for Never, Void, or other common types; oftentimes there's no fundamental need for these to be special cases to begin with.

5 Likes

Maybe no fundamental need, but certainly there is often ergonomic advantages of being able to e.g. make dealing with Void a little easier. A common extension on Result is this:

public extension Result where Success == Void {
    static var success: Result { return .success(()) }
}

It allows you to write stuff like return .success instead of return .success(())

Or a some store that is generic over some key, may have a function to retreive a value using fetch() instead of the much more awkward fetch(()). For some of these instances, it's also nice to hide the awkward version.

Needed? No. Ergonomic? Sure!

(I what I would really like, is for a the type system to suggest foo(_:Void) as a candidate when looking for a matching overload for call site foo())

3 Likes

This is definitely one of those things that seems like it should be possible to the point where it’s just infuriating that it’s not. You build up a mental model of what you can do and then take a small logical step out of it and suddenly it doesn’t work.

(you can use == in a constraint) + (you can use != in place of == to get the inverted outcome anywhere) = (you can’t use != in a constraint) :man_shrugging:

1 Like

The type system is really powerful in the current version of Swift and of course the use for the proposed feature would be limited. Just as @sveinhal put it it’s just ergonomics.

The reason why I started this discussion was to see if the community had any interesting ideas about the proposed feature. Of course, the Never and Void types example is not really powerful or motivational, it was just meant to highlight the lack of the proposed constraint operators. But such a weak example certainly doesn’t justify such an addition.

I'd say that there's no need to have (multiple or single) trailing closure syntax but it helps with the ergonomics. So I don't think "just" ergonomics isn't enough to justify an addition.

Sorry, to have mislead you. What I meant was that I don’t find my example on its own a good enough reason to add the proposed feature. I assume that the implementation of such a feature has already been discussed and rejected as being too complex for a probably rarely used feature. If an efficient implementation of such a feature were available I’d look more into this in detail, but at the current state of things I don’t think it’s worth it.

It has mentioned at least once before, pre-forums, and you could search for more by "non-conformance."

Here is a use-case I just came across where it would be great to have non-conformance:

extension Comparable where Self == Strideable {
	func borders(on other: Self) -> Bool {
		other == self.advanced(by: 1) || self == other.advanced(by: 1)
	}
}

extension Comparable where Self != Strideable {
	func borders(on other: Self) -> Bool { false }
}

Unless there is a different way of doing this?

I would think that having both extensions would work the way you'd expect, without the need for != Strideable, as the more specific extension should be active in the appropriate contexts.

5 Likes