Nested generic trouble

it’s reasonable to assume that when @stuchlej asked about

public struct Container<T: Encodable> {
  public var value: T

  public func bytes(_ val: Int) { print("Int") }
  public func bytes(_ val: String) { print("String") }
  public func bytes<A: Encodable>(_ val: A) { print("A") }

  public func store() {
    bytes(value)
  }
}

he was asking why the generics weren’t inlined into

public struct _Container_Int {
  public var value: Int

  public func bytes(_ val: Int) { print("Int") }
  public func bytes(_ val: String) { print("String") }
  public func bytes<A: Encodable>(_ val: A) { print("A") }

  public func store() {
    bytes(value)
  }
}

before type checking, which would allow the call to bytes(_:) inside store to resolve to the one that prints "Int". the call to store would never have any resilience overhead, because it just calls the (Int) -> () overload.

you are completely correct that there are lots of downsides to inlining generics before typechecking, probably more than the amount of upsides to it, but one of those upsides is that using _Container_Int never incurs resilience overhead.

i personally think that swift made the right choice in typechecking generics instead of specializations, and that inlining generics before typechecking was never going to scale. but my point this entire time has been that inlining after typechecking doesn’t work very well today, because we don’t have tools in the language today to statically assert that @inlinable has been correctly applied to all participants in a call chain that clients who do not care about resilience might call.

let’s walk through how protocol-based dispatch might work for Container<Int>.

protocol HasBytesTypeName
{
    static
    var name:String { get }
}
extension HasBytesTypeName where Self:Encodable
{
    public static
    var name:String { "A" }
}
extension Int:HasBytesTypeName
{
    public static
    var name:String { "Int" }
}

then we would refactor Container<T> into something like:

public
struct Container<T> where T:HasBytesTypeName & Encodable
{
    public
    var value:T

    public
    func bytes<Value>(_ value:Value)
        where Value:HasBytesTypeName & Encodable
    {
        print(Value.name)
    }
    public
    func store()
    {
        self.bytes(self.value)
    }
}

now, how would we use such a type?

import ContainerModule

let container:Container<Int> = ...
container.store()

in the absence of @inlinable, this call to store would call the unspecialized implementation, because Container doesn’t know what types Container<T> it might be asked to construct. this is going to be slow.

sometimes we do not care, because we do not consult type metadata in the implementation, or we do consult type metadata but the implementation does so many other things that the overhead of the generics is unimportant. but oftentimes we do care, and it would be helpful if the type system could help with things like diagnosing missing @inlinables.

2 Likes