Accessing particular elements in a variadic tuple

I'm finding myself needing a facility to access specific elements in a variadic tuple from time to time.

Currently, I'm doing something like this:

struct VariadicTupleAccessor<each Element>: Sendable {

  init() {
    var offsetCursor = 0
    func referenceNextElement<T>(as type: T.Type) -> ElementReference<T> {
      let alignment = MemoryLayout<T>.alignment
      let offset = (offsetCursor + alignment - 1) & ~(alignment - 1)
      offsetCursor = offset
      return ElementReference(offset: offset)
    }
    elementReferences = (repeat referenceNextElement(as: (each Element).self))
  }

  let elementReferences: (repeat ElementReference<each Element>)

  struct ElementReference<T> {
    fileprivate let offset: Int
  }
  func mutate<T, Result>(
    _ reference: ElementReference<T>,
    on tuple: inout (repeat each Element),
    body: (inout T) throws -> Result
  ) rethrows -> Result {
    try withUnsafeMutableBytes(of: &tuple) { buffer in
      assert(reference.offset < buffer.count)
      let pointer = (buffer.baseAddress! + reference.offset)
        .assumingMemoryBound(to: T.self)
      return try body(&pointer.pointee)
    }
  }

}

Is there a current or future language feature that can make this possible without unsafe pointers?
Does the logic I have here make sense? It makes some assumptions about the ABI of tuples but I think that is OK?

1 Like

As far as I know, the ABI of tuples (in general, but particularly for heterogeneous element types) is not guaranteed, and therefore unsafe to make assumptions about.

Are you against using key paths (or closures) for this? You'd get something similar in effect — a key-like reference you can carry around to look up an associated value.

Tuples in generic contexts will always have their elements stored in order. This offset computation doesn't look right:

You need to advance offsetCursor = offset + MemoryLayout<T>.size to advance past the current element.

As @mattcurtis noted, this looks roughly WritableKeyPath shaped, so it would be interesting to hear if key paths aren't sufficient for your use case.

1 Like

You need to advance offsetCursor = offset + MemoryLayout<T>.size to advance past the current element.

Thanks for catching this, you are correct! I'm currently fighting some compiler crashes so haven't actually been able to run this code yet...

As @mattcurtis noted, this looks roughly WritableKeyPath shaped, so it would be interesting to hear if key paths aren't sufficient for your use case.

This is interesting, because I also thought that key paths would be the correct API to get this to work. I just don't think that in modern swift there is a way to get to a WritableKeyPath from a (repeat each T). Essentially I want to iterate a parameter pack and get a WritableKeyPath<(repeat each T), SpecificT> for each element in the parameter pack, but I'm not sure this is something that is possible right now, hence the memory-offset-calculation-shenanigans.

Here's the two places I actually use this pattern in my code at the moment:

Neither of these work now, the tuple example causes a compiler crash and the object properties example can't prove that the properties pack is the same shape as the references pack, but those are unrelated issues I just haven’t had a moment to fix yet.

If I'm understanding what you're trying to do, one approach you might be able to adapt is mapping over tuples to produce new tuples:

func set<Value, each P>(_ newValue: Value, at index: Int, in tuple: inout (repeat each P)) {
    var localIndex = 0
    
    func mapNewValue<T>(oldValue: T) -> T {
        defer {
            localIndex += 1
        }
        
        if localIndex == index {
            return newValue as! T
        }
        
        return oldValue
    }
    
    tuple = (repeat mapNewValue(oldValue: each tuple))
}

There's some inefficiencies here (like no in-place mutation), but the general approach might help.

1 Like

Is there a way to say “map this tuple, but just change this one element”? The way I see it if you have a variadic tuple (a, b, c, …z) you can convert it to (foo(a), foo(b), foo(c), …foo(z)) but there is no way to express “change just b to foo(b)” when you don’t have any handle for b other than the variadic parameter.

The way you suggested kinda works, but is indeed inefficient and has its own unsafety (force casting).

Not yet, that I know of.

Yeah... there are things that can make it safer, like comparing types, but either way you'll have to find a means to workaround the limitation of not being able to dynamically index and mutate individual values of tuples.

Here’s a subscriptable wrapper, courtesy of all your examples.

struct Pack<each Element> {
  var elements: (repeat each Element)

  init(_ element: repeat each Element) {
    self.elements = (repeat each element)
  }

  subscript<T>(position: Int) -> T {
    get {
      let byteOffset = byteOffset(of: T.self, at: position)
      return withUnsafeBytes(of: self.elements) { buffer in
        buffer
          .baseAddress
          .unsafelyUnwrapped
          .advanced(by: byteOffset)
          .assumingMemoryBound(to: T.self)
          .pointee
      }
    }
    mutating set {
      let byteOffset = byteOffset(of: T.self, at: position)
      withUnsafeMutableBytes(of: &self.elements) { buffer in
        let ptr = buffer
          .baseAddress
          .unsafelyUnwrapped
          .advanced(by: byteOffset)
          .assumingMemoryBound(to: T.self)

        ptr.pointee = newValue
      }
    }
  }

  private static func memoryLayout<T>(of: T.Type) -> MemoryLayout<T>.Type {
    MemoryLayout.self
  }

  private func byteOffset<T>(of type: T.Type, at position: Int) -> Int {
    return withUnsafeBytes(of: self.elements) { buffer in
      var byteOffset = 0
      var index = 0

      for metatype in repeat (each Element).self {
        let layout = Self.memoryLayout(of: metatype)

        // Align to this element.
        do {
          let remainder = byteOffset % layout.alignment
          if remainder != 0 {
            byteOffset += layout.alignment - remainder
          }
        }
        guard index == position else {
          // Plus the size of this element, and keep going.
          byteOffset += layout.size
          index += 1
          continue
        }

        precondition(
          metatype == T.self,
          "Expected '\(T.self)', got '\(metatype)'"
        )

        return byteOffset
      }

      preconditionFailure("Index out of range")
    }
  }
}

do {
  var pack = Pack(1, true, 3, "hello", 5)

  print(pack[3] as String) // hello
  pack[3] = "world"
  print(pack[3] as String) // world
}

But yeah, I don’t think we can make this any more memory- or type-safe today.

5 Likes

Thanks!
One thing I'd be worried about with this design is that it seems pretty easy to accidentally subscript as the wrong type which would lead to undefined behavior.

I finally settled on a model where I have an "archetype" which you can pseudo-iterate to get accessors which are guaranteed to be typed properly. The new code is here: SwiftClaude/Sources/Tool/Input/Support/Variadic Tuple Archetype.swift at 9882ab71ea7bdedf42b9723ad1a4297419a4a1c8 · GeorgeLyon/SwiftClaude · GitHub

I had a more-type safe variant but I had to do a ton of nasty stuff to get the compiler to not crash and to believe that the pack of accessors and the pack of values was the same shape.