Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does type(of:) return Metatype, rather than T.Type?

Tags:

types

swift

I noticed that type(of:) has this unexpected signature:

func type<T, Metatype>(of value: T) -> Metatype

whereas I would have expected:

func type<T>(of value: T) -> T.Type

The actual signature somehow takes 2 independent and unbounded type parameters, uses one of them as the parameter type, and the other as the return type. This seems to suggest that I can do something silly like this:

let foo: String = type(of: 1) // T is Int, Metatype is String

But when I actually tried it,

Cannot convert value of type 'Int.Type' to specified type 'String'

So I don't get to specify Metatype after all, even though it's a generic type parameter. I was curious how the compiler did this, so I went to the source code and checked. I think what's preventing me from doing that silly thing is this @_semantics annotation:

@_transparent
@_semantics("typechecker.type(of:)")
public func type<T, Metatype>(of value: T) -> Metatype {

I also saw some comments explaining that the implementation written here isn't actually called, and that this is directly handled by the type checker itself. But that doesn't answer my question - why does type(of:) return a completely unrelated type parameter Metatype, rather than T.Type?

My guess is that there is some edge case where type(of:) will return a completely unrelated type to T, but I have no idea what that is.

like image 393
Sweeper Avatar asked Apr 12 '21 00:04

Sweeper


People also ask

What is a Metatype?

Definition of metatype : a topotype or homeotype determined by the original author of its species.

What is a Metatype in Swift?

A metatype type refers to the type of any type, including class types, structure types, enumeration types, and protocol types. The metatype of a class, structure, or enumeration type is the name of that type followed by .

What is the return type of a method?

In computer programming, the return type (or result type) defines and constrains the data type of the value returned from a subroutine or method. In many programming languages (especially statically-typed programming languages such as C, C++, Java) the return type must be explicitly specified when declaring...

What is the importance of return type in Java?

Importance of return type in Java? A return statement causes the program control to transfer back to the caller of a method. Every method in Java is declared with a return type and it is mandatory for all java methods. A return type may be a primitive type like i nt, float, double, a reference type or void type (returns nothing).

What is a return type in C++?

A return type may be a primitive type like int, float, double, a reference type or void type(returns nothing). There are a few important things to understand about returning the values The type of data returned by a method must be compatible with the return type specified by the method.

What is the return type of a subroutine in Java?

In the Java example: the return type is int. The program can therefore rely on the method returning a value of type int. Various mechanisms are used for the case where a subroutine does not return any value, e.g., a return type of void is used in some programming languages:


Video Answer


1 Answers

tl;dr: The behavior of type(of:) depends on whether T is existential or concrete, and the type system can't effectively reflect the actual return type syntactically, so it's handled directly in the type checking system. Metatype is specifically not bound in code to be the same as T so that the effective behavior can be specialized. Metatype and T are not necessarily related.


type(of:) is special in that its behavior differs depending on the type passed into it. Specifically, it has special behavior for existential types by being able to reach through the existential box to get the underlying type of the value passed in. For example:

func myType<T>(of value: T) -> T.Type {
    return T.self
}

protocol Foo {}
struct X: Foo {}

let x = X()
print(type(of: x), "vs.", myType(of: x)) // => X vs. X

let f: Foo = X()
print(type(of: f), "vs.", myType(of: f)) // => X vs. Foo

When given an existential type like Foo, a return type of T.Type could only return the metatype of the existential itself (i.e. Foo.self), as opposed to the metatype of the value inside of the existential container (X.self). So instead of returning T.Type, type(of:) returns an unrelated type Metadata which is bound to the correct type in the type checker itself. This is the edge case you were looking for:

My guess is that there is some edge case where type(of:) will return a completely unrelated type to T, but I have no idea what that is.

If you look in lib/Sema/TypeChecker.h, you can see some special semantics declarations for several stdlib function types:

/// Special-case type checking semantics for certain declarations.
enum class DeclTypeCheckingSemantics {
  /// A normal declaration.
  Normal,

  /// The type(of:) declaration, which performs a "dynamic type" operation,
  /// with different behavior for existential and non-existential arguments.
  TypeOf,

  /// The withoutActuallyEscaping(_:do:) declaration, which makes a nonescaping
  /// closure temporarily escapable.
  WithoutActuallyEscaping,

  /// The _openExistential(_:do:) declaration, which extracts the value inside
  /// an existential and passes it as a value of its own dynamic type.
  OpenExistential,
};

The key one here is TypeOf, which is indeed returned for functions with the @_semantics("typechecker.type(of:)") attribute you noted. (You can see how that attribute is checked in TypeChecker::getDeclTypeCheckingSemantics)

If you go looking for usages of TypeOf, there are two key locations in type-checking:

  1. getTypeOfReferenceWithSpecialTypeCheckingSemantics which injects the type constraint in the type checker constraint system. type(of:) is handled here as an overload, because Metadata isn't actually bound; the constraint solver here applies an effective type checking constraint which constrains Metadata to be the actual type of value. The key here is that type(of:) is written in this way so that it would be an overload, and handled here.
  2. ExprRewriter::finishApply which performs the actual expression rewriting in the AST to replace the return type with the effective actual type of the value

From (1):

// Proceed with a "DynamicType" operation. This produces an existential
// metatype from existentials, or a concrete metatype from non-
// existentials (as seen from the current abstraction level), which can't
// be expressed in the type system currently.

Pulling back some history — this was implemented back in commit 1889fde2284916e2c368c9c7cc87906adae9155b. The commit message from Joe is illuminating:

Resolve type(of:) by overload resolution rather than parse hackery.

type(of:) has behavior whose type isn't directly representable in Swift's type system, since it produces both concrete and existential metatypes. In Swift 3 we put in a parser hack to turn type(of: <expr>) into a DynamicTypeExpr, but this effectively made type(of:) a reserved name. It's a bit more principled to put Swift.type(of:) on the same level as other declarations, even with its special-case type system behavior, and we can do this by special-casing the type system we produce during overload resolution if Swift.type(of:) shows up in an overload set. This also lays groundwork for handling other declarations we want to ostensibly behave like normal declarations but with otherwise inexpressible types, viz. withoutActuallyEscaping from SE-0110.

Since then, as we can see from WithoutActuallyEscaping and OpenExistential, other special functions have been rewritten to take advantage of this.

like image 102
Itai Ferber Avatar answered Nov 25 '22 18:11

Itai Ferber