Parameter packs, same-shape requirements, and spreading arrays

A few things that I wish I could do with parameter packs appear to be impossible. I'll outline my use case here.

I'm working with SwiftSyntax, looking for instances of layer.cornerRadius = blah:

let list = CodeBlockItemListSyntax(node.statements.map { item in
    guard let sequenceExpr = item.item.as(SequenceExprSyntax.self),
                        // helper function to turn .elements into `[ExprSyntax]`
            let exprs = expressions(sequenceExpr.elements, expecting: 3),

            // is assignment
            let _ = exprs[1].as(AssignmentExprSyntax.self),

            let memberAccess = exprs[0].as(MemberAccessExprSyntax.self),
            let layerAccess = memberAccess.base.as(DeclReferenceExprSyntax.self),
            // last member access is `cornerRadius`
            memberAccess.declName.baseName.text == "cornerRadius",
            // previous member access is `layer`
            layerAccess.baseName.text == "layer",
            let assignmentValue = exprs[2].as(ExprSyntax.self)
    else {
        // Not a corner radius assignment, skip
        return item
    }

    ...
}

I'd like to tidy up this boilerplate with variadic generics somehow. Ideally I should be able to do this:

// imagined signature of `tuplify`
func tuplify<T, each U>(_ value: [T]) -> (repeat each U)

...

let list = CodeBlockItemListSyntax(node.statements.map { item in
    guard let sequenceExpr = item.item.as(SequenceExprSyntax.self),
            let (
                memberAccess: MemberAccessExprSyntax,
                _: AssignmentExprSyntax,
                assignmentValue: ExprSyntax,
            ) = tuplify(sequenceExpr.elements), // pretend .elements is an array

            let layerAccess = memberAccess.base.as(DeclReferenceExprSyntax.self),
            // last member access is `cornerRadius`
            memberAccess.declName.baseName.text == "cornerRadius",
            // previous member access is `layer`
            layerAccess.baseName.text == "layer"
    else {
        // Not a corner radius assignment, skip
        return item
    }

    ...
}

This currently isn't possible because you can't pass in [T] in place of a repeat each T parameter. And Swift doesn't have a way to "spread" the array to fit it, either. Doing so would require a runtime check as to whether the run-time length of the array matches the compile-time length of the pack.

I can see how this use case was overlooked, but I wish an affordance had been included. Onto my next attempt...

Let's try this instead:

// new imagined signature of `tuplify`
func tuplify<each T, each U>(_ value: repeat each T) -> (repeat each U)

...

let list = CodeBlockItemListSyntax(node.statements.map { item in
    guard let sequenceExpr = item.item.as(SequenceExprSyntax.self),
            let s = sequenceExpr.elements,  // pretend `s` is an array
            let (
                memberAccess: MemberAccessExprSyntax,
                _: AssignmentExprSyntax,
                assignmentValue: ExprSyntax,
            ) = tuplify(s[0], s[1], s[2]),

            let layerAccess = memberAccess.base.as(DeclReferenceExprSyntax.self),
            // last member access is `cornerRadius`
            memberAccess.declName.baseName.text == "cornerRadius",
            // previous member access is `layer`
            layerAccess.baseName.text == "layer"
    else {
        // Not a corner radius assignment, skip
        return item
    }

    ...
}

I can't even do this, I assume because only one pack is used in the signature, so it for some reason can't guarantee that they have the same size? The error is Pack expansion requires that 'each T' and 'each U' have the same shape.

Also, you can't cast between each T and each U, as far as I can tell.

Here's how I tried to implement the last tuplify():

func tuplify<each T, each U>(_ value: repeat each T) -> (repeat each U) {
    return (repeat each value as! each U)
}

Are there any other ways to accomplish what I want here, or am I outta luck here?

I can't tell precisely what you are trying to do, but I'm pretty sure you've got options.

My best guess without any more input is this, which is kind of a pain because of how Swift handles labels.

@Test func test() throws {
  let multipleTypes: [Any] = [1, true, "three"]
  typealias UnlabeledPrefix = (Int, Bool, String)
  unlabeled: do {
    let prefix = multipleTypes.tuplePrefix() as UnlabeledPrefix?
    #expect(try #require(prefix) == (1, true, "three"))
  }
  labeled: do {
    typealias Prefix = (a: Int, b: Bool, c: String)
    let prefix = multipleTypes.tuplePrefix(UnlabeledPrefix.self) as Prefix?
    #expect(try #require(prefix) == (1, true, "three"))
  }
}
extension Sequence {
  func tuplePrefix<each Element>(
    _: (repeat each Element).Type = (repeat each Element).self
  ) -> (repeat each Element)? {
    var iterator = makeIterator()
    return try? (repeat { _ in try cast(iterator.next()) } ((each Element).self))
  }
}

struct CastError: Error { init() { } }
func cast<Uncast, Cast>(_ uncast: Uncast) throws(CastError) -> Cast {
  guard case let cast as Cast = uncast else { throw .init() }
  return cast
}

Wow, that is exactly what I wanted to do, thank you so much!

I guess not having to spell out the second generic by extending Sequence itself helps, and you were able to use the "unused" pack as a parameter by typing it as .Type, very clever.

Thanks again :)

1 Like

I didn't know you could do this as an expression, what is this doing exactly? Or, where is it documented?

(repeat { _ in
    …
} ((each Element).self))

Ideally, this could work:

var iterator = makeIterator()
return try? (repeat cast(iterator.next()))

But you can't use repeat without a pack reference. The metatype works.

func next<Next>(_: some Any) throws -> Next { try cast(iterator.next()) }
return try? (repeat next((each Element).self))

This is just that next function, as a closure:

{ _ in try cast(iterator.next()) }

As for creating an unlabeled tuple metatype from a labeled one—I feel sure that needs a macro. I am only learning them. Please point us to it if you know it or write one!

You know what? Given how ugly it ends up being otherwise, maybe something closer to that idea is the best available spelling.

return try? (repeat cast(iterator.next()) as each Element)

We don’t have a syntax to directly state a same-shape requirement, but we infer same-shape requirements whenever a function’s parameter or return type contains a pack expansion with more than one pack reference in it. So here for example:

func zip<each T, each U>(…) -> (repeat (each T, each U))

One of the positions where we infer same-shape requirements happens to be the where clause itself. So when all else fails, you can state a useless conformance requirement to Any:

func weirdThing<each T, each U>(_: repeat each T) -> (repeat each U)
    where (repeat (each T, each U)): Any

This is also how we print same-shape requirements in swiftinterface files, where we must make all inferred requirements explicit.

3 Likes

You can eliminate the immediately-applied closure by using a tuple expression and then discarding all elements but the “real” one that you want, eg repeat (foo(), each …).0.