Homogeneous tuples

SE-0393: Value and Type Parameter Packs mentions "Tuple conformances" in its "future direction" section. To not sidetrack that thread let me present a raw idea about tuples here.


Homogenious Tuples

Motivation

Currently it is not possible to declare homogeneous tuples (a tuple of the same typed elements) other than doing it explicitly:

var value = (0, 0, 0, 0, 0) // a tuple with 5 Ints

This becomes unpractical quickly:

// a tuple of 1234 Ints
var value = (0, 0, 0, /* ... 1230 elements skipped... */, 0)

Why large† homogeneous tuples are important:

  • compared to Arrays: tuples are POD types with no memory allocation, locks or ARC overhead (there's no internal reference object sitting in them) which makes them highly appealing in certain contexts (e.g. realtime code).
  • compared to structs: tuple layout is known and guaranteed (all elements packed back to back according to their stride). This is important when working with fixed memory layouts (e.g. treating raw memory as a tuple of n bytes).
  • compared to UnsafePointers: tuples could be considered "safer".

† - we use "large" here in the same sense as what "large number of elements in a struct" is. For example we can have a tuple (or a struct) with 1000 elements, but going much further from it (say 10K) would be a stretch for all components involved (compiler, runtime, stack usage requirements). Similar to a limit on a number of elements in a struct, while it is impractical to hardcode a particular hard limit on the number of tuple elements (arity), as a guideline we could define it to be "a few thousand elements tops". Note that tuple size is roughly count * element_stride, so the practical limit on the count should take this into account. This is very similar to the (unspoken but well appreciated) limit on struct elements: while you may have a struct with 1000 byte-sized fields and use for your "stack-allocated" variables, it would be unwise to have a struct of 1000 CATransform3D fields (128 bytes each) as a stack allocated variable.

Beyond impracticality of its declaration today it's hard to work with large tuples as there is no subscript operation (other than "subscripting by a constant index") or getting tuple arity or its element type ††.

(†† we could use type(of: value.0) if we have a value but no way of getting element type from a tuple type itself).

Note that very little is required from the language / standard library to have a complete homogeneous tuple support. Just this: ability to declare a large homogeneous tuple and specify its element type and count – everything else could be derived from this).

Here's a sketch example how we enhance this:

Base homogeneous tuple support

One possibility is to have homogeneous tuple definition looking like a generic type (almost):

typealias Foo = Tuple<Int, let count: 1234>
let foo: Foo = .init(repeating: 0, count: 1234)
let foo = Foo<Int, let count: 1234>(repeating: 0, count: 1234)
print(Foo.Element) // Int

This is basically it! Everything else (see below) could be derived.

Suggestions or alternatives on how to achieve this base functionality are greatly appreciated.


Derived homogeneous tuples functionality

Everything in this section could be derived from the above or similar base tuple support functionality.

Tuple arity

Foo.count
foo.count

Possible tuple arity implementation:

    static var count: Int {
        // last element is "Element.size" sized, all other elements are "Element.stride" sized.
        1 + (MemoryLayout<Self>.size - MemoryLayout<Element>.size) / MemoryLayout<Element>.stride 
    }
    var count: Int { Self.count }

Alternatively, if only "instance" count is needed we may do so without referencing "Element" type:

    var count: Int {
        1 + (MemoryLayout.size(ofValue: self) - MemoryLayout.size(ofValue: self.0) / MemoryLayout.stride(ofValue: self.0))
    }

Tuple subscript

foo[42] // same as value.42
var i = 42
foo[i] = 24

Possible tuple subscript implementation:

extension Tuple {
    subscript (_ index: Int) -> Element {
        get {
            precondition(index >= 0 && index < count)
            var v = self
            return withUnsafeBytes(of: &v) { p in
                let r = p.baseAddress!.assumingMemoryBound(to: Element.self)
                return r[index]
            }
        }
        set {
            precondition(index >= 0 && index < count)
            withUnsafeMutableBytes(of: &self) { p in
                let r = p.baseAddress!.assumingMemoryBound(to: Element.self)
                r[index] = newValue
            }
        }
    }
}

Tuple Hashable conformance

Possible tuple hashability implementation:

extension Tuple: Hashable where Element: Hashable {
    func hash(into hasher: inout Hasher) {
        for i in 0 ..< count {
            hasher.combine(self[i])
        }
    }
}

Tuple Equatable conformance

Possible tuple equatability implementation:

extension Tuple: Equatable where Element: Equatable {
    public static func == (lhs: Self, rhs: Self) -> Bool {
        for i in 0 ..< lhs.count {
            if lhs[i] != rhs[i] {
                return false
            }
        }
        return true
    }
}

Tuple Collection conformance (TBD)

6 Likes

I seem to remember that this was part of another proposal and I thought that was already approved. What happened to that one?

It was approved as SE-0283, but ran into some implementation issues that caused it to be backed out: Implementation issues with SE-0283 (Tuples are EHC)

I don't think the let makes sense. It should be just typealias Foo = Tuple<Int, count: 1234>.

or maybe reuse some of the syntax from SE-0393, if repeat is accepted, with Tuple<repeat(1234) Int>

1 Like

I was just going to suggest that. It seems like a reasonable future for fixed size packs, too.

What is the Element type of Void? Regardless of the answer, it seems like its count would be 1 instead of 0 using the provided definition.

Why do you need to repeat 'count' in the init. This can be a source of error. Or may be 'count' can be différent in the init but I don't see what that could be means.

typealias Foo = Tuple<Int, let count: 1234>
let foo: Foo = .init(repeating: 0)
let foo = Foo<Int, let count: 1234>(repeating: 0)
print(Foo.Element) // Int
3 Likes

Previous related pitch from @Michael_Gottesman: [Pitch] Improved Compiler Support for Large Homogenous Tuples (2021-05)

1 Like

Vector may be a better name for such types (i.e. it's something in between a fixed size array and a homogeneous tuple, but neither of them exactly, and therefore deserves its own name)

1 Like