[swift-evolution] [Discussion] Seal `T.Type` into `Type<T>`

Adrian Zubarev adrian.zubarev at devandartist.com
Wed Jul 13 04:55:40 CDT 2016


Here is a part of a draft I put together in case we should go for a review on this.

Here is the formatted version if you cannot read it here: https://gist.github.com/DevAndArtist/a8d11011a376d47d0afc941017e64c75

Proposed solution

I propose to seal T.Type into a standalone type Type<T> and disallow .Type in public declaration usage.

Behavior Changes:

Make a distinction between internal and public .self.

Rename internal version of .self to .metatype or revision SE–0090 for Swift 3 and leave .self internally.

Make public .self (if not dropped for Swift 3) construct an instance ofType<T> instead a metatype T.Type.

Update all codebase to use Type<T> instead of T.Type.

Small example:

- public func sizeof<T>(_: T.Type) -> Int
+ public func sizeof<T>(_: Type<T>) -> Int
     
    // Current implementation
-   return Int(Builtin.sizeof(T.self))

    // Possible internal implementation
+   return Int(Builtin.sizeof(T.metatype))
     
    // Possible implementation with `Type<T>`
+   return Int(Builtin.sizeof(Type<T>.metatype))
}
Pros:

Removing ambiguity in array/dictionary shorthand syntax.
Extensibility around the metatype:
Conformance to Hashable protocol.
Direct access to metatype related information like size, stride or aligment:
Type<T>.size equivalent to current sizeof(T.self)
Type<T>.stride equivalent to current strideof(T.self)
Type<T>.alignment equivalent to current alignof(T.self)
A step to remove public .self magic.
Less magical types floating around the codebase.
Better future maintenance.
Cons:

A distinction between internal and public .self must be introduced.

Dynamic types must be tweaked.

Currently it’s possible to check the Type like this:

//  
func isInt<T>(metatype: T.Type) -> Bool {
    return metatype is Int.Type
}
is cast must be tweaked after this proposal:

func isInt<T>(type: Type<T>) -> Bool {
          
    return type == Type<Int>()
              
    // Since we store/pass a type (name) in this check,
    // we can safely convert `Int` into an instance of `Type<Int>`
    //
    // The result would look like this:
              
    return type == Int  
             
    // This is logically equivalent to `metatype is Int.Type`
}
Type<T> adds more code noise for declarations.
T.Type = 6 characters
Type<T> = 7 characters
Type<T> adds a bottleneck (static) property to access the metatype:
Direct access today T.self (in future T)
Metatype access after this proposal:
Direct access through static member: Type<T>.metatype
Access through an initialized instance: typeInstance.metatype
Detailed Design

Rules:

Whenever a type (name) is passed or stored, it will be implicitly converted to Type<SomeType> (no need for .self - SE–0090).
Declarations do not follow the rule #1.
These rules would imply the following behavior and potentially solve the ambiguity in array/dictionary shorthand syntax:

// Type: Type<T>
let variable1 = T  

// Type: T
let variable2 = T.init()

// Type: T
let variable3 = T()  

// Return-type of static member from T
let variable4 = T.staticMember

// Return-type of an Array<T> static member
let variable6 = [T].staticMember

// Return-type of a Dictionary<T, U> static member
let variable7 = [T: U].staticMember

// Type: Array<T>
let array1 = [T]()

// Type: Array<T>
let array2: [T] = []

// Type: Array<Type<T>>
let array3: [Type<T>] = []

// Type: Array<Type<T>>
let array4 = [Type<T>]()

// Type: Dictionary<T, U>
let dictionary1 = [T: U]()

// Type: Dictionary<T, U>
let dictionary2: [T: U] = [:]

// Type: Dictionary<Type<T>, U>
let dictionary3: [Type<T>: U] = [:]

// Type: Dictionary<Type<T>, U>
let dictionary4 = [Type<T>: U]()
Possible Implementation Design:

///
public struct Type<T> : Hashable, CustomStringConvertible, CustomDebugStringConvertible {

    /// Creates an instance of `Type<T>`.
    public init() {
     
        // Possible new internal access to the metatype
        self.metatype = T.metatype
         
        // Same hash calculation like in `ObjectIdentifier`
        let rawPointerMetatype = unsafeBitCast(T.metatype, to: Builtin.RawPointer.metatype)
        self.hashValue = Int(Builtin.ptrtoint_Word(rawPointerMetatype))
         
        // Iff the value of `size`, `stride` and `alignment` properties cannot
        // be changed due runtime, we could move the calculation directly to
        // the initializer and make these properties to constants!
    }
     
    ///
    public let metatype: T.Type  
     
    /// The Type<T>'s hash value.
    ///
    /// Hash values are not guaranteed to be equal across different executions of
    /// your program. Do not save hash values to use during a future execution.
    public let hashValue: Int  
     
    /// Returns the contiguous memory footprint of `T`.
    ///
    /// Does not include any dynamically-allocated or "remote" storage.
    /// In particular, when `T` is a class type, is the
    /// same regardless of how many stored properties `T` has.
    public var size: Int {
        return sizeof(self)
         
        // OR:
        return Int(Builtin.sizeof(T.metatype))
    }
     
    /// Returns the least possible interval between distinct instances of
    /// `T` in memory.  The result is always positive.
    public var stride: Int {
        return strideof(self)
         
        // OR:
        return Int(Builtin.strideof_nonzero(T.metatype))
    }
     
    /// Returns the minimum memory alignment of `T`.
    public var alignment: Int {
        return alignof(self)
     
        // OR:
        return Int(Builtin.alignof(T.metatype))
    }
         
    /// A textual representation of `self`.
    public var description: String {
        return "Type<\(self.metatype)>"
    }
     
    /// A textual representation of `self`, suitable for debugging.
    public var debugDescription: String {
        return "<" + self.description
                   + " metatype: \(self.metatype)"
                   + " size: \(self.size)"
                   + " stride: \(self.stride)"
                   + " alignment: \(self.alignment)>"
    }
}

/// Static members for access metatype specific information
/// without constructing an instance of `Type<T>`:
///
/// - `Type<T>.metatype`
/// - `Type<T>.size`
/// - `Type<T>.stride`
/// - `Type<T>.alignment`
public extension Type {
     
    ///
    public static var metatype: T.Type {
        return T.metatype
    }
     
    /// Returns the contiguous memory footprint of `T`.
    ///
    /// Does not include any dynamically-allocated or "remote" storage.
    /// In particular, when `T` is a class type, is the
    /// same regardless of how many stored properties `T` has.
    public static var size: Int {
        return sizeof(self)
         
        // OR:
        return Int(Builtin.sizeof(T.metatype))
    }
     
    /// Returns the least possible interval between distinct instances of
    /// `T` in memory.  The result is always positive.
    public static var stride: Int {
        return strideof(self)
         
        // OR:
        return Int(Builtin.strideof_nonzero(T.metatype))
    }
     
    /// Returns the minimum memory alignment of `T`.
    public static var alignment: Int {
        return alignof(self)
     
        // OR:
        return Int(Builtin.alignof(T.metatype))
    }
}

public func ==<T, U>(lhs: Type<T>, rhs: Type<U>) -> Bool {
    return lhs.hashValue == rhs.hashValue
}


-- 
Adrian Zubarev
Sent with Airmail

Am 10. Juli 2016 um 18:03:17, Adrian Zubarev (adrian.zubarev at devandartist.com) schrieb:

Just to be crystal clear, here is my vision.

Currently Swift is heading in this direction:

SomeType.staticMember == metatypeInstance.staticMember == SomeType.self.staticMember
SomeType.init == metatypeInstance.init == SomeType.self.init

SomeType.self -> SomeType.Type (metatype)

SomeType -> SomeType.Type (removed .self)

SomeType.self is SomeType.Type == true

func foo<T>(metatype: T.Type) -> T {
      
    // lets pretend T conforms to an initializable protocol   
    return metatype.init()   
}
With some tweaking and a little tradeoff for Type<T> we’d get this model:

SomeType.staticMember == Type<SomeType>.metatype.staticMember
SomeType.init == Type<SomeType>.metatype.init

SomeType -> Type<SomeType>() (initialized instance; removed .self)

SomeType == Type<SomeType>() == true

SomeType is Type<SomeType>() == true

func foo<T>(type: Type<T>) -> T {
      
    print(type.size)
    print(type.debugDescription)
    print(type.hashValue)
      
    // lets pretend T conforms to an initializable protocol   
    return type.metatype.init()   
}


-- 
Adrian Zubarev
Sent with Airmail

Am 10. Juli 2016 um 17:28:35, Adrian Zubarev (adrian.zubarev at devandartist.com) schrieb:

Can you point me to some reading? I’m curious what exactly you’re talking about!?

—

I found a negative side effect of sealing T.Type. The problem appears lies in the dynamic is cast.

Currently we could do something like this:

func isInt<T>(metatype: T.Type) -> Bool {
       
    return metatype is Int.Type
}
There is no way to express the same type check with Type<T> except this:

func isInt<T>(type: Type<T>) -> Bool {
       
    return type == Type<Int>()
       
    // since this check is something where we store/pass a type (name)
    // we could implicitly convert `Int` into an instance of `Type<Int>`
    //
    // The result would look like this:
       
    return type == Int // which is logically equivalent to `metatype is Int.Type`
}


-- 
Adrian Zubarev
Sent with Airmail

Am 10. Juli 2016 um 16:24:50, L. Mihalkovic (laurent.mihalkovic at gmail.com) schrieb:


It looks like this is touching on a small subset of the not-yet-designed reflection API (still tabled for 4.0?). I am interested to see what balance it strikes between declarations (eg. being able to reflect extensions declarations), types (eg reflecting type conformance), and operations (xxx.newInstance(), xxx.setValue()). The mirror api shows a general direction, maybe there is a way to squeeze a tiny subset in 3.0 (just like was done with P&Q that will remain a degenerate case of the upcoming more general syntax). Maybe just enough for people not to have to use ObjectIdentifier(:Any.Type).

Regards
(From mobile)

On Jul 10, 2016, at 2:22 PM, Adrian Zubarev via swift-evolution <swift-evolution at swift.org> wrote:

Hello Chris,

my main concern for this change is the lack of extensibility of metatypes. We can access the metatype through the .self postfix, which potentially will be removed in the future (only the postfix). Through an instance of such a metatype we can then access the static members and all initializer for our type.

SomeType.staticMember equals metatypeInstance.staticMemeber
SomeType.init equals metatypeInstance.init
It is also possible to extract some more information from the metatypes with the help of Buildin wrapper functions like sizeof or strideof. And the last but not the least we can use metatypes in type checking scenarios.

Last weak I asked the community if there is a need for all metatypes conform to the Hashable protocol, which would be really handy. The overall response was positive, even if only a handful of people responded.

The whole idea of metatypes conforming to Hashable kept me thinking how this might be solved in the future. The main problem is that metatypes provide direct access to initializer and static members of the type and a conformance to Hashable seems to be impossible without some compiler magic or a hidden hashValue property. The main confusion here might be that metatypes can be stored (at least while runtime) inside variables and pretend to be instances of a T.Type type.

That said, I’d like to consider of sealing the direct access to a metatype into a real type for the sake of extensibility and clarity.

I’m aware that my bikeshedding introduced more boilerplate in terms of accessing the initializer and static members through a bottleneck property called sealed. Furthermore there is one more character to type if we’d go with Type<T> instead of T.Type.

This is why this is a discussion before making any further decisions. :)

PS: As I showed in my last code example a wrapper type Type<T> can also provide a few more information about the metatype.

Type<T>.size
Type<T>.stride
Type<T>.alignment
Furthermore sealing T.Type into Type<T> does solve the problem with the array/dictionary shorthand syntax.

Only when one would store/pass a type (name) we’d implicitly convert SomeType to Type<SomeType>.

Declarations do not follow this implicit conversion:

func foo(_: SomeType) is not equivalent to func foo(_: Type<SomeType>)
Array<SomeType>() == [SomeType]() but is not equivalent to [Type<SomeType>]() == Array<Type<SomeType>>()
Here is again my bikeshedding:

public struct Type<T> : Hashable, CustomStringConvertible, CustomDebugStringConvertible {
        
    // `metatype` is a better name than `sealed`
    // The tradeoff is this bottleneck property

    public let metatype: T.Type
         
    public let hashValue: Int
         
    public static var size: Int { get }
    public static var stride: Int { get }
    public static var alignment: Int { get }
         
    public var size: Int { get }
    public var stride: Int { get }
    public var alignment: Int { get }
         
    public var description: String { get }
    public var debugDescription: String { get }
}

public func ==<T, U>(lhs: Type<T>, rhs: Type<U>) -> Bool


-- 
Adrian Zubarev
Sent with Airmail

Am 10. Juli 2016 um 00:18:02, Chris Lattner (clattner at apple.com) schrieb:

Hi Adrian,

What’s the motivation for this change?

-Chris
_______________________________________________
swift-evolution mailing list
swift-evolution at swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160713/af28f8b7/attachment.html>


More information about the swift-evolution mailing list