Variadic types: expressing containment in parameter packs?

I'm sure that this has been discussed, planned, maybe discarded or postponed even, but I can't come up with the proper terms to find this :)

The basic idea is:

  • Given a Foo<each T>
  • How could we (eventually) express that the list of T's contains another type, like extension Foo where (repeat each T).contains(ConcreteType.self) { ... }?

Illustrating with an Example

Imagine you have a container of typed dependencies like so:

struct Context<each Dependency> {
    let dependencies: (repeat each Dependency)
    init(_ dependencies: repeat each Dependency) {
        self.dependencies = (repeat each dependencies)
    }
}

Then it'd be possible to put a couple of "dependencies" into a concrete context:

let appContext = Context(DB(), HTTP(), Logging())

This would fit into a logging function that uploads data to HTTP that was fetched from a DB like this:

func uploadUserName(
    userID: UserID
) -> (Context<DB, HTTP, Logging>) async -> Bool

However, if you use a function to read the user name from the DB like this:

func getUserName(userID: UserID) -> (Context<DB>) -> String

... then there's no simple way to express on the type level that you can get from Context<DB, HTTP, Logging> to Context<DB>.

Note that it's currently (Swift 6.1) perfectly possible to convert one into the other at run time:

func uploadUserName(
    userID: UserID
) -> (Context<DB, HTTP, Logging>) async -> Bool
    return { context in
        // Make a Context<DB> from the Context<DB,_,_>:
        let name = getUserName(userID: userID)(Context(context.dependencies.0))
        context.dependencies.2.log("-- Uploading user name")
        do {
            try await context.dependencies.1.upload(name)
            context.dependencies.2.log("Upload finished successfully")
            return true
        } catch {
            context.dependencies.2.log("Upload finished with error: \(error)")
            return false
        }
    }
}

Getting to pack elements more nicely?

You'll notice that the positional tuple value access is a bit cumbersome.

context.dependencies.1.upload(name)
context.dependencies.2.log("Upload finished successfully")

You can't tell what 0, 1, 2 are without inferring from the method that's being called, or looking at the type declaration further to the top.

So you might want to reach for a typed accessor that allows:

context[HTTP.self].upload(name)

However, it appears that currently you end up with either

  • a partial function (accepting wider input that in can handle, producing runtime errors)
  • an optional or throwing function (expressing that you don't know at compile time)

Variant A: Partial function that throws

extension Context {
    subscript<ADependency>(_ DepType: ADependency.Type) -> ADependency {
        get {
            for dependency in repeat each self.dependencies {
                if let d = dependency as? ADependency {
                    return d
                }
            }
            preconditionFailure("Dependency missing for type \(DepType.self)")
        }
    }
}

This allows you to write:

func uploadUserName(
    userID: UserID
) -> (Context<DB, HTTP, Logging>) async -> Bool {
    return { context in
        let name = getUserName(userID: userID)(Context(context[DB.self]))
        context[Logging.self].log("-- Uploading user name")
        do {
            try await context[HTTP.self].upload(name)
            context[Logging.self].log("Upload finished successfully")
            return true
        } catch {
            context[Logging.self].log("Upload finished with error: \(error)")
            return false
        }
    }
}

But even though you know that Context<DB, HTTP, Logging> contains a value of each of the types DB, HTTP, Logging, this API permits shooting yourself in the foot:

func getUserName(userID: UserID) -> (Context<DB>) -> String {
    return { context in
        let name = context[DB.self].userName(userID: userID)
        // Let me just also log this real quick ... 
        context[Logging.self].log("Found name \(name) for ID \(userID)")
        //  ⚠️ runtime error! Forgot to declare Context<DB, Logging>
        return name
    }
}

Variant B: Expressing uncertainty with optional or errors

So instead of a partial function, define subscript to either return nil, or express you expect this to work with a Result/throwing an error:

extension Context {
    struct DependencyMissing: Error { init() {} }

    subscript<ADependency>(_ DepType: ADependency.Type) -> ADependency {
        get throws {
            for dependency in repeat each self.dependencies {
                if let d = dependency as? ADependency {
                    return d
                }
            }
            throw DependencyMissing()
        }
    }
}

But that will make all call-sites worse.

You know when you have access to the DB that getting to the DB will not fail. So unwrapping try? or do-catch-ing is cumbersome.

But if you express your confidence with force try!, you end up with a partial function again.

Solution: Don't use nice getters, stick to what the compiler knows

When we want to lean on the compiler, this seems to be the best we have:

context.dependencies.1.upload(name)
context.dependencies.2.log("Upload finished successfully")

You can't compile code that accesses context.dependencies.999 in a Context with just 3 elements in the pack.

Yes, if you reorder types in the pack, you need to change the call site to reflect that.

But unless we can expose e.g. type containment like so:

// Purely fictional code:
extension Context where (repeat each Dependency).contains(DB.self) {
    var db: DB { 
        for dependency in repeat each self.dependencies {
            if let d = dependency as? DB {
                return d
            }
        }
        fatalError("Should not be reachable")
    }
}

... I see no better way out.

2 Likes

Does the Context actually need to be generic? What if you just directly store a DB, HTTP, and Logging in a struct?

1 Like

Good news: it's possible to do what you want: BrainF*** in the Swift type system (proves that it's possible; doesn't show you directly how to achieve it)
Bad news: it doesn't integrate nicely with variadics AFAIK; I think you'd at least need pack destructuring a la [Pitch] Pack Destructuring & Pack Splitting
Worse news: my approach is horrific for compile times

1 Like

Thanks for the suggestion. I'm not blocked on writing apps by this. :slight_smile: What you suggest would work.

I wanted to experiment and see whether Swift's type system could express "this function eventually needs to run in an environment with DB, HTTP and Logging set up to run", sort of inspired by Haskell type classes that get all the love when it comes to expressing dependencies.

1 Like

I love this, thank you. I knew this language was useful! Could you as the expert on this maybe try to create a new typed language in Swift's type system next to express my original ideas better? :troll:

1 Like

Haskell type classes are just Swift protocols, so what you're thinking of is expressed using a generic function, for example:

protocol DB {...}
protocol HTTP {...}
protocol Logging {...}

func foo(db: some DB, http: some HTTP, logging: some Logging) {
  ...
}

You can also package them up like this:

protocol Context {
  associatedtype DBType: DB
  var db: DBType { get }

  associatedtype HTTPType: HTTP
  var http: HTTPType { get }

  associatedtype LoggingType: Logging
  var logging: LoggingType { get }
}

func foo(context: some Context) {
  ...
}
2 Likes

OK, here's what you're after, using the "recursive dynamicMemberLookup" approach from the "BrainF***" thread:

struct ServiceNamespace {}

protocol ABCService {}
struct ABC: ABCService {}
extension ServiceNamespace {
    var abcService: ABCService { fatalError() }
}

protocol DEFService {}
struct DEF: DEFService {}
extension ServiceNamespace {
    var defService: DEFService { fatalError() }
}

protocol XYZService {}
struct XYZ: XYZService {}
extension ServiceNamespace {
    var xyzService: XYZService { fatalError() }
}

protocol ServiceLocator {}
struct EmptyServiceLocator: ServiceLocator {}
@dynamicMemberLookup
struct Locate<Service, Others: ServiceLocator>: ServiceLocator {
    var service: Service
    var others: Others
    
    subscript(dynamicMember keyPath: KeyPath<ServiceNamespace, Service>) -> Service {
        service
    }
    
    subscript<OtherService>(dynamicMember keyPath: KeyPath<Others, OtherService>) -> OtherService {
        others[keyPath: keyPath]
    }
}

protocol ServiceRequirement {
    associatedtype Locator: ServiceLocator
}
struct NoRequirements: ServiceRequirement {
    typealias Locator = EmptyServiceLocator
}
struct Require<Service, Others: ServiceRequirement>: ServiceRequirement {
    typealias Locator = Locate<Service, Others.Locator>
}

@dynamicMemberLookup
struct
Resolver<
    Services: ServiceLocator,
    PartiallyResolved: ServiceLocator,
    FullyResolved: ServiceLocator,
    RemainingRequirements: ServiceRequirement,
    InServicesHaystack: ServiceLocator,
> {
    var services: Services
    var partiallyResolved: PartiallyResolved
    var fullyResolved: FullyResolved
    var inServicesHaystack: InServicesHaystack
    
    // when we find the next requirement at the head of the haystack
    subscript<
        Resolved,
        Needle,
        RequirementsTail: ServiceRequirement,
        HaystackTail: ServiceLocator,
    >(dynamicMember keyPath: KeyPath<
        Resolver<
            Services,
            Locate<Needle, PartiallyResolved>,
            FullyResolved,
            RequirementsTail,
            Services
        >,
        Resolved
    >) -> Resolved
    where
        FullyResolved == EmptyServiceLocator,
        RemainingRequirements == Require<Needle, RequirementsTail>,
        InServicesHaystack == Locate<Needle, HaystackTail>
    {
        Resolver<
            Services,
            Locate<Needle, PartiallyResolved>,
            FullyResolved,
            RequirementsTail,
            Services
        >(
            services: services,
            partiallyResolved: Locate(
                service: inServicesHaystack.service,
                others: partiallyResolved
            ),
            fullyResolved: fullyResolved,
            inServicesHaystack: services
        )[keyPath: keyPath]
    }
    
    // when we skip the head of the haystack for being the wrong type
    subscript<
        Resolved,
        DiscardedService,
        HaystackTail: ServiceLocator,
    >(dynamicMember keyPath: KeyPath<
        Resolver<
            Services,
            PartiallyResolved,
            FullyResolved,
            RemainingRequirements,
            HaystackTail
        >,
        Resolved
    >) -> Resolved
    where
        FullyResolved == EmptyServiceLocator,
        InServicesHaystack == Locate<DiscardedService, HaystackTail>
    {
        Resolver<
            Services,
            PartiallyResolved,
            FullyResolved,
            RemainingRequirements,
            HaystackTail
        >(
            services: services,
            partiallyResolved: partiallyResolved,
            fullyResolved: fullyResolved,
            inServicesHaystack: inServicesHaystack.others
        )[keyPath: keyPath]
    }
    
    // reverse PartiallyResolved into FullyResolved
    subscript<
        Resolved,
        LastService,
        EarlierServices: ServiceLocator,
    >(dynamicMember keyPath: KeyPath<
        Resolver<
            Services,
            EarlierServices,
            Locate<LastService, FullyResolved>,
            NoRequirements,
            InServicesHaystack
        >,
        Resolved
    >) -> Resolved
    where
        RemainingRequirements == NoRequirements,
        PartiallyResolved == Locate<LastService, EarlierServices>
    {
        Resolver<
            Services,
            EarlierServices,
            Locate<LastService, FullyResolved>,
            NoRequirements,
            InServicesHaystack
        >(
            services: services,
            partiallyResolved: partiallyResolved.others,
            fullyResolved: Locate(service: partiallyResolved.service, others: fullyResolved),
            inServicesHaystack: inServicesHaystack
        )[keyPath: keyPath]
    }
}

extension Resolver where
    RemainingRequirements == NoRequirements,
    PartiallyResolved == EmptyServiceLocator
{
    // when we succeed
    var resolved: FullyResolved {
        fullyResolved
    }
}

extension Resolver where
    InServicesHaystack == Services,
    PartiallyResolved == EmptyServiceLocator,
    FullyResolved == EmptyServiceLocator
{
    init(services: Services, requirements: RemainingRequirements.Type) {
        self.services = services
        self.partiallyResolved = EmptyServiceLocator()
        self.fullyResolved = EmptyServiceLocator()
        self.inServicesHaystack = services
    }
}

extension ServiceLocator {
    func resolve<Requirements: ServiceRequirement>(_ requirements: Requirements.Type) -> Resolver<
        Self,
        EmptyServiceLocator,
        EmptyServiceLocator,
        Requirements,
        Self
    > {
        Resolver(services: self, requirements: requirements)
    }
}

@main struct Main {
    static func main() {
        let serviceRegistry = Locate(
            service: ABC() as ABCService, others: Locate(
            service: XYZ() as XYZService, others: EmptyServiceLocator()))
        print(serviceRegistry)
        print("----")
        
        do {
            // reversed
            typealias Requirements = Require<XYZService, Require<ABCService, NoRequirements>>
            let services = serviceRegistry.resolve(Requirements.self).resolved
            print(services)
        }
        do {
            // in order
            typealias Requirements = Require<ABCService, Require<XYZService, NoRequirements>>
            let services = serviceRegistry.resolve(Requirements.self).resolved
            print(services)
        }
        do {
            // subset (tail)
            typealias Requirements = Require<XYZService, NoRequirements>
            let services = serviceRegistry.resolve(Requirements.self).resolved
            print(services)
        }
        do {
            // subset (head)
            typealias Requirements = Require<ABCService, NoRequirements>
            let services = serviceRegistry.resolve(Requirements.self).resolved
            print(services)
        }
#if false
        do {
            // irreconcilable
            typealias Requirements = Require<DEFService, Require<ABCService, NoRequirements>>
            let services = serviceRegistry.resolve(Requirements.self).resolved
            print(services)
        }
#endif
    }
}