Inspecting generic constraints

It is not always obvious what the constraints are on a generic type or function, and I think it would be nice to have some way to programmatically discover them. I am envisioning this as a way to help programmers learn and understand things, as that is my primary use-case. In other words, I want to be able to print out a textual description of the generic constraints on something.

There are a few reasons this is valuable. First, just to be able to easily see how something is declared. And second, because sometimes the compiler applies additional constraints above and beyond what is written in source code, which can be unexpected and non-obvious. For example, see @DevAndArtist’s thread What kind of magic behavior is this?.

I recently encountered a similar situation while working on a super serious and important feature. Specifically, I wrote the following code:

infix operator --> : RangeFormationPrecedence

func --> <T: Comparable> (lhs: T, rhs: T) -> ReversedCollection<Range<T>> {
  return (rhs ..< lhs).reversed()
}

Now, what types does the --> operator work with? The only constraint listed is T: Comparable, so it should work for anything Comparable, right?

But wait, you can only form a ReversedCollection from a Collection, and Range is only a Collection when its Bound is Strideable with an integer Stride. (Aside: how is a person expected to discover that if they don’t already know it? Yet another use-case for generic inspection!)

So why is my declaration even legal? After all, I haven’t provided enough constraints to ensure the return type is sensible. Not a problem, the compiler is happy to silently and, er, helpfully, insert the constraints on its own!

Observe:

let a = 5 --> 0         // works
let b = 5.0 --> 0.0     // fails
// error: type 'Double' does not conform to protocol 'SignedInteger'

So, in my playground where I’m testing this, I want to print out all the constraints on --> that the compiler knows about. It would be nice to write something like Mirror(reflecting: -->), but of course that doesn’t work.

I want to be able to do this sort of inspection not only for generic functions, but also for generic types, and protocols with associated types. Essentially anywhere there might be a where clause, I want to be able to see what the compiler knows about it.

4 Likes

As an alternative, you can inspect derived constraints post-type-checking by pretty-printing ASTs with
swiftc -print-ast.

In Xcode, this can also be done by option-clicking on a declaration:

2 Likes

It just says “No Quick Help” when I do that.

I forgot to mention – these IDE features might still not work in playgrounds :man_shrugging:

EDIT
...and specifically for operators.

Aside from ensuring the LSP exposes this kind of metadata, there’s not much we can do about it here. I mean, we can post about these kinds of ideas and there’s probably a fair chance someone on the Xcode team might see it, but Xcode isn’t part of the Swift project (which I’m kinda sad about, but is probably for the best... prevents the language from being formally dependent on the editor).

Additional weirdness:

If I remove the Comparable constraint from my previous example, leaving T unconstrained, then the operator function still compiles, but the attempt to use it on Int fails with “error: operator is not a known binary operator”.

I would have expected the compiler to infer Comparable just as it infers Strideable, since T is used as the Bound of a Range, which must be Comparable. I do not understand why I am required to write some of the constraints but not others.

Interestingly, the inference does work for non-operator functions:

func foo<T>(_ high: T, _ low: T) -> ReversedCollection<Range<T>> {
  return (low..<high).reversed()
}

foo(5, 0)     // no error

Edit: …and now it is letting me use the operator with no constraints as well. Not sure what was causing it to error out before.

ReversedCollection requires the Base is BidirectionalCollection.

Range has the hidden requirements for that.

Yes, if you remove Comparable, it will pick it up from SignedInteger.

Weird, I am not able to reproduce this neither on Xcode 10.2 or 10.1. What Xcode version are you using? Is it a playground?