Can't get parameter pack code to compile

Hey everyone, I'm struggling to get some parameter pack code to compile.

I'm trying to make a variadic route builder, so that I can do things like:

let router = Router()
let group = router.group()

group
    // by default, closures are a "(Request) -> Response" type
    .on(.get, "/about") { request -> Response in ... }

group
    // this value becomes a parameter to the closure
    .addingParameter(someContext) 
    // the handler now takes two parameters
    .on(.get, "/with/context") { request, context -> Response in ... }

group
    // add a context parameter
    .addingParameter(someContext) 
    // validate the request, supply an account parameter
    .requiringUserAuthorization() 
    // the handler now takes three parameters
    .on(.get, "/user/account") { request, context, account -> Response in ... }

I've gotten the pattern of this reduced down to this struct. The parameter pack (each Parameter) represents all the additional parameters that are passed to the .on(...) closure, in addition to the initial Request value.

When the builder gets the .on(...) method called, it uses the register closure to package up that closure and notify another object (the previous builder, or ultimately the Router) about it.

(note: I've left out the "method" and "path" parameters for brevity)

struct Builder<each Parameter> {
    typealias Handler = (Request, repeat each Parameter) -> Response
    typealias Registrar = (@escaping Handler) -> Void
    
    let register: Registrar

    func addParameter<P>(_ newParam: P) -> Builder<repeat each Parameter, P> {
        typealias NewBuilder = Builder<repeat each Parameter, P>
        let newRegistrar: NewBuilder.Registrar = { (newHandler: NewBuilder.Handler) in
            self.register({ (request: Request, myParams: repeat each Parameter) -> Response in
                return newHandler(request, (repeat each myParams, newParam))
            })
        }
        return NewBuilder(register: newRegistrar)
    }

    func requiringUserAuthorization() -> Builder<repeat each Parameter, Account> { ... }
}

This looks right to me, but I cannot get this to compile. I have tried:

  • removing typealiases
  • adding a typealias Parameters = repeat each Parameter
  • making the addParameter return type be Builder<(repeat each Parameter, P)>: this crashes the compiler
  • removing parenthesis on the newHandler(request ...) call
  • adding type annotations everywhere
  • probably other things I've forgotten

As the code currently stands above, the compiler produces two errors on the newHandler(request, (repeat ...)) line:

- Cannot pass value pack expansion to non-pack parameter of type 'repeat each Parameter'
- Missing argument for parameter #3 in call

Any ideas what I'm missing?

Edit: I'm on Xcode 15.3 (Swift version 5.10 - 5.10.0.13) on macOS Sonoma, in case that matters

You're calling newHandler as if it had the type (Request, (repeat each Parameter, P)) -> Response (a function of two arguments, the second one being a tuple) but the type of newHandler is actually (Request, repeat each Parameter, P) -> Response (it takes three arguments).

So I would expect that changing the call to newHandler(request, repeat each myParams, newParam) would fix the error, but you said you already did that:

So maybe there's a bug. Can you share a self-contained test case?

1 Like

Adding this to the top will make the code self-contained:

struct Request {}
struct Response {}
struct Account {}

Same error (with removing parens) on Xcode 15.4 beta, btw.

3 Likes

You're absolutely right, and this demonstrates the problem:

func apply<each XS, X>(_ xs: repeat each XS, x: X, fn: (repeat each XS, X) -> ()) {
  fn(repeat each xs, x)
}

We need to handle function values differently from named functions. Right now the implementation uses the "label matching" rule for both: swift-evolution/proposals/0393-parameter-packs.md at main · apple/swift-evolution · GitHub

However since the types of function values don't have labels, we need to use the "type matching" rule described by the proposal instead.

1 Like

Here's a workaround. First declare this:

func apply<each T, U>(_ fn: (repeat each T) -> U, _ t: repeat each T) -> U {
  return fn(repeat each t)
}

Here's the original version and the change:

struct Request {}
struct Response {}
struct Account {}

struct Builder<each Parameter> {
    typealias Handler = (Request, repeat each Parameter) -> Response
    typealias Registrar = (@escaping Handler) -> Void

    let register: Registrar

    func addParameter<P>(_ newParam: P) -> Builder<repeat each Parameter, P> {
        typealias NewBuilder = Builder<repeat each Parameter, P>
        let newRegistrar: NewBuilder.Registrar = { (newHandler: @escaping NewBuilder.Handler) in
            self.register({ (request: Request, myParams: repeat each Parameter) -> Response in
                // Original version:
                // return newHandler(request, (repeat each myParams, newParam))
                // Correct but hits type checker bug:
                // return newHandler(request, repeat each myParams, newParam)
                return apply(newHandler, request, repeat each myParams, newParam)
            })
        }
        return NewBuilder(register: newRegistrar)
    }

    func requiringUserAuthorization() -> Builder<repeat each Parameter, Account> { fatalError() }
}

I also had to mark the newHandler parameter @escaping, because it's captured by the escaping closure passed to self.register().

2 Likes

@Slava_Pestov Thank you very much! After your previous comment about function values versus named functions, I tried wrapping up the handler itself into its own type:

struct Builder<each Parameter> {
    fileprivate let register: (Method, String, Handler<repeat each Parameter>) -> Void
    
    func addParameter<P>(_ newParam: P) -> Builder<repeat each Parameter, P> {
        typealias NewBuilder = Builder<repeat each Parameter, P>
        let newRegistrar: (Method, String, Handler<repeat each Parameter, P>) -> Void = { (method: Method, path: String, newHandler: Handler<repeat each Parameter, P>) in
            
            let trampoline = Handler<repeat each Parameter>(invocation: { (request: Request, mostParameters: repeat each Parameter) in
                return try await newHandler.invoke(for: request, parameters: repeat each mostParameters, newParam)
            })
            
            self.register(method, path, trampoline)
        }
        return NewBuilder(register: newRegistrar)
    }
}

private struct Handler<each Parameter> {
    let invocation: (Request, repeat each Parameter) async throws -> Response
    
    func invoke(for request: Request, parameters: repeat each Parameter) async throws -> Response {
        return try await invocation(request, repeat each parameters)
    }
}

This compiles as expected!

… now I just need to wrestle with some compiler crashes during silgen :sweat_smile:

3 Likes

Please file some GitHub issues if you have time, parameter packs need some exercising.

5 Likes

You folks are awesome.