How to pass `nil` as an "optional" parameter pack?

Hi! I'm experimenting with parameter packs (and variadic types) and I am looking for the ability to pass an "optional" parameter pack with nil.

Here is an example that compiles:

struct S1<each T> {
  var a: (repeat each T)
  
  init(a: repeat each T) {
    self.a = (repeat each a)
  }
}

let _ = S1<Int, Int, Int>(a: 1, 2, 3)

What I am actually looking for is the ability to pass nil as a parameter pack option. I can specify an optional expanded tuple… but the synthesized init expects an optional expanded tuple (I lose the ability to init with a pack directly):

struct S2<each T> {
  var a: (repeat each T)?
}

let _ = S2<Never>(a: nil)
let _ = S2<Int, Int, Int>(a: 1, 2, 3)
                          `- error: extra arguments at positions #2, #3 in call

I am not sure how to write my own init that would take a parameter pack (that is not expanded as a tuple):

struct S3<each T> {
  var a: (repeat each T)?
  
  init(a: Optional<repeat each T>) {
                   `- error: pack expansion 'repeat each T' can only appear in a function parameter list, tuple element, or generic argument of a variadic type
    self.a = (repeat each a)
  }
}

Taking a look at SE-0393 and SE-0398… I'm not completely clear if this is something that:

  • Should be currently supported and I'm doing it wrong?
  • Something that is not currently supported but there might be a workaround?
  • Something that is not currently supported with no known workaround?

I'm building from 5.10… but have the ability to wrap this code with a requirement on 6.0 if that helps. Thanks!

Can you represent the nil case with an empty pack instead?

let _ = S2< >()
2 Likes

Hmm… another piece I would be missing is that (eventually) my variadic type items might conform to an arbitrary protocol that (AFAIK) would not be adopted by Void:

struct S1<each T> where repeat each T : Equatable {
  var a: (repeat each T)
  
  init(a: repeat each T) {
    self.a = (repeat each a)
  }
}

let _ = S1<Int, Int, Int>(a: 1, 2, 3)
let _ = S1<Void>(a: ())
        `- error: type 'Void' does not conform to protocol 'Equatable'
let _ = S1<Never>(a: ())
                     `- error: cannot convert value of type '()' to expected argument type 'Never'

I don't believe the empty pack could help me here if I would expect Never to pass my conformance checks on Equatable here. Hmm…

Ahh… wait… actually…

struct S1<each T> where repeat each T : Equatable {
  var a: (repeat each T)
  
  init(a: repeat each T) {
    self.a = (repeat each a)
  }
}

let _ = S1< >()

This compiles without error.

1 Like

Indeed, S1<>, S1<Void> and S1<Never> are all distinct types.

2 Likes

Hmm… something I would not be clear about with an empty pack workaround is how to refactor code that would (otherwise) be doing an easy conditional control flow on whether or not that pack was nil:

struct S1<each T, U> where repeat each T : Equatable {
  let a: (repeat each T)?
  
  init(a: repeat each T) {
    self.a = (repeat each a)
  }
  
  func f() {
    if let a = self.a {
      print("a != nil")
    } else {
      print("a == nil")
    }
  }
}

AFAIK there is not an easy way to check for foo == ()? Correct? Is this something that might come later along with explicit pack syntax and same-element requirements?

There's no direct way to check if a pack is empty at runtime, but you can do something like this:

struct Pack<each T> {
  static var isEmpty: Bool {
    for _ in repeat (each T).self {
      return false
    }
    return true
  }
}

print(Pack< >.isEmpty)
print(Pack<Int>.isEmpty)
8 Likes

And those calls to isEmpty even optimize completely down to a single instruction. Chef's kiss.

I think it would be a big educational win to curate a collection of these kinds of pack introspection tricks in official documentation somewhere—a list of small tools that do one important thing well. Years of dealing with C++ template shenanigans have trained me to see problems through that kind of lens. The Swift answer to the same questions often looks completely different, but also a lot easier to understand at a glance, so whether someone is new to variadic generics or experienced with another language, everyone would benefit from retraining their brain to think in the right terms.

8 Likes

I didn't know this was possible to express. Is there any way to do it the way shown here, without a space, or is the suggestion that it is possible just a bug?

There’s a long standing parser bug where the space is required when you utter the type in expression context, but we really ought to fix that.

3 Likes

In this case it would be nice if this Pack type was defined in the standard library. It could also expose a count property to return the length — that’s not something you can express efficiently right now in user code, but we have a compiler intrinsic for it in the Builtin module.

2 Likes

The same trick works just fine, and optimizes just as well: Compiler Explorer

4 Likes

This is a great idea! And got me thinking… is there any ability to attempt and perform similar logic (checking for empty pack) with a compile time specialized method?

struct S<each T> {
  let a: (repeat each T)?
  
  init(a: repeat each T) {
    self.a = (repeat each a)
  }
  
  func f() {
    print("a is not empty")
  }
  
  func f() where T is empty {
    print("a is empty")
  }
}

That does not compile… is there any legit way (assuming I want to try and build from 5.10 for now) to perform a check like that? Is this something we would need to wait for same element requirements to ship?

Even if you could do this, you usually wouldn't want to, because it wouldn't work through another layer of indirection:

struct S2<each T> {
  let inner: S<repeat each T>

  func test() {
    inner.f() // would always print "a is not empty",
    // because at this location T is not *statically* known to be empty.
  }
}

Overload resolution happens once per call site (as written in the source).

1 Like

I was looking for a version of this is_empty runtime check that would build from 5.10. Here is what I started with:

fileprivate struct NotEmpty: Error {}

fileprivate func isEmpty<each Element>(_ element: (repeat each Element)) -> Bool {
  func _isEmpty<T>(_ t: T) throws {
    throw NotEmpty()
  }
  do {
    repeat try _isEmpty(each element)
  } catch {
    return false
  }
  return true
}

Unfortunately… this never returns true. It looks like the _isEmpty helper is "calling" even when the pack is empty… which is not what I expected.

Actually this one does work… but I had to change how I called it:

let element = (repeat each Element)

if isEmpty((repeat each self.element)) == false

Instead of:

let element = (repeat each Element)

if isEmpty(self.element) == false

Ahh… correct. That's good to keep in mind. Thanks!