Why are tuples allowed to be only partially initialized?

The following is, I think, primarily a curiosity that I came across, but it got me wondering – what is Swift's model for the initialization of tuples? Nominal types all seemingly must be fully initialized before use, but tuples appear to allow "partial initialization" (of local variables at any rate), so long as no use of an uninitialized element occurs. E.g. this kind of thing is allowed:

struct S {
  var p1, p2: Int
}

func test() {
  let tup: (Int, Int)
  tup.0 = 1
  print(tup.0) // ✅ okay, despite tup.1 uninitialized

  let s: S
  s.p1 = 1 // 🛑 Struct 's' must be completely initialized before a member is stored to
}

And since you don't have to initialize the entire value, you can have an even stranger situation where the type itself isn't even able to be fully initialized:

let neverSayNever: (Bool, Never)
neverSayNever.0 = true
print(neverSayNever.0) // ✅ true

From having looked at how the definite initialization logic works, it makes a bit of sense to me why things could behave this way, but it's not entirely clear if there's a motivating reason for it. Is it intentional that tuples are treated differently in this respect than other types?

9 Likes

Thank you. I have just discovered that variables too don't have to be initialised until they are used.

func test() {
  var u: Int, v: Int
  u = 1
  print (u)
  print (v) // Error: Variable 'v' used before being initialized
}
1 Like

Another interesting consequence is that variables of type ()/Void don't need to be initialized at all:

func test() {
    let tup: ()
    print(tup)
}

It does seem to be intentional that tuples are treated specially. Specifically, DIMemoryUseCollector.h in the compiler code base seems to document that tuple elements are always analyzed separately, while struct and class stored properties are only analyzed separately when in an initializer:

/// This struct holds information about the memory object being analyzed that is
/// required to correctly break it down into elements.
///
/// This includes a collection of utilities for reasoning about (potentially
/// recursively) exploded aggregate elements, and computing access paths and
/// indexes into the flattened namespace.
///
/// The flattened namespace is assigned lexicographically.  For example, in:
///   (Int, ((Float, (), Double)))
/// the Int member is numbered 0, the Float is numbered 1, and the Double is
/// numbered 2.  Empty tuples don't get numbered since they contain no state.
///
/// Structs and classes have their elements exploded when we are analyzing the
/// 'self' member in an initializer for the aggregate.
///
/// Derived classes have an additional field at the end that models whether or
/// not super.init() has been called or not.
class DIMemoryObjectInfo {
1 Like

Apparently, it used to be that structs were also allowed to be partially initialized, before it became disallowed in 2013 with commit 28e7245 with the following description:

definitive initialization is not going to be field sensitive for structs anymore:

structs are going to be required to be completely initialized through a constructor
before used. Start removing some complexity around them.

It also seems like partial initialization has basically always been allowed. The tracking of tuple and struct elements was introduced in commit 5b3ddf7, before this tracking was even used for anything, such as when it became an error to use uninitialized memory in commit 615079a.

So, to make an educated guess, tuples are allowed to be partially initialized simply because there's no reason not to allow it. And structs aren't allowed to be partially initialized because, unlike tuples, structs can have initializers that enforce invariants, and it's useful to check each initializer of a struct to see which invariants are enforced.

5 Likes

Interesting! I suppose that makes some amount of sense, since Void is an empty tuple so there's not really anything to initialize (and IIUC all Void instances are effectively "the same"). It's a little bit like how you also don't have to explicitly initialize optional variable declarations like var opt: Int? to nil.

Thanks for sharing your "archaeological" findings. Initially I thought this behavior was inconsistent with the rest of the language (which, in a certain way it is), but upon further reflection I think it's okay. Element-wise initialization is how nominal types are allowed to behave within their initializers, so maybe since tuples don't have explicit initializers, this is actually consistent and desirable behavior. It still is a bit odd that you can declare a type that never gets "fully" initialized, but I think the worst case here is perhaps that the optimizer has to do a bit of work to eliminate some dead code. It would make sense to me to diagnose these cases just because they are probably unintentional (and I can't think of a good reason why you'd want to not fully initialize a tuple), but perhaps this is enough of an edge case even diagnosing it wouldn't be that valuable.

2 Likes