Is there a way to spread a tuple into the arguments of a function?

A quick search only got me the following StackOverflow question but it was an XY problem so while OP's problem was solved the question remains unanswered.

I'm finding myself in the same situation but I cannot modify the types. It boils down to the following simplified example:

func call<each Arg, Result, Failure>(
	_ f: (repeat each Arg) throws(Failure) -> Result,
	with args: (repeat each Arg)
) throws(Failure) -> Result {
	// ???
}

I have a tuple with the arguments of a function which expects multiple arguments. I would need to spread the tuple into the arguments of the function.

I believe Swift used to be able to pass a tuple as arguments but it was removed.

1 Like

Correct, it used to be called a tuple splat. RIP.

https://www.hackingwithswift.com/swift/2.2/tuple-splat

I wish we had something like the following:

func myFunction(one: Int, two: Int, three: Int) {}

var tuple = (1,2,3)

myFunction.invoke(with: tuple)

Maybe one day.

1 Like

Also, have a look at Is Implicit Tuple Splat behavior not fully removed?

There is a way around this (kinda). I am trying to see if I still use this behavior in my own code. You can see the discussion.

1 Like

I wish we had something like the following:

I was thinking of a prefix operator on tuples that would turn it into a parameter pack, then you can use it like any other parameter packs.

I used ... here just to stay similar to parameter packs, it could be something else or even a built-in free function.

func f(_ one: Int, _ two: Int, _ three: Int) {}
let tuple = (1,2,3)

f(repeat each ...tuple)

That operator would have a signature of prefix operator ...<each T>(tuple: (repeat each T)) -> each T, this isn't valid Swift as of now as the parameter packs cannot be return values.

Also, have a look at Is Implicit Tuple Splat behavior not fully removed?

Very interesting! I don't have to make my function generic for input, it can take a single argument.

// this works
func tuplify<I, O, F>(_ f: @escaping (I) throws(F) -> O) -> (I) throws(F) -> O { f }
func test(_: Int, _: Int, _: Int) {}

tuplify(test)((1,2,3))

However, the following fails with the following error:

  • Cannot convert value of type (repeat each Arg) throws(Failure) -> Result to expected argument type ((repeat each Arg)) throws(Failure) -> Result
  • Pack expansion requires that (repeat each Arg) and each Arg have the same shape
func call<each Arg, Result, Failure>(
	_ f: @escaping (repeat each Arg) throws(Failure) -> Result, with args: (repeat each Arg)
) throws(Failure) -> Result {
	tuplify(f)(args)
}

Oops, I've crashed the compiler. Posted an issue on the repo.

struct MemberwiseConverter<each Input: BitwiseCopyable, Output: BitwiseCopyable> {
	@usableFromInline
	let _initializer: ((repeat each Input)) -> Output

	@usableFromInline
	init(_ initializer: @escaping ((repeat each Input)) -> Output) {
		_initializer = initializer
	}

	@inlinable @inline(__always)
	public func apply(_ input: (repeat each Input)) -> Output {
		try _initializer(input)
	}

	@inlinable @inline(__always)
	public func unapply(_ output: Output) -> (repeat each Input) {
		guard canCast(Output.self, (repeat each Input).self) else {
			fatalError()
		}
		return unsafeBitCast(output, to: (repeat each Input).self)
	}
}

@usableFromInline
func canCast<I: BitwiseCopyable, O: BitwiseCopyable>(_: I.Type, _: O.Type) -> Bool {
	MemoryLayout<I>.alignment == MemoryLayout<O>.alignment && MemoryLayout<I>.size == MemoryLayout<O>.size
}

The following works:

struct Memberwise<Input: BitwiseCopyable, Output: BitwiseCopyable, Failure: Error>: Conversion {
	@usableFromInline
	let _initializer: (Input) throws(Failure) -> Output

	@usableFromInline
	init(_ initializer: @escaping (Input) throws(Failure) -> Output) {
		_initializer = initializer
	}

	@inlinable @inline(__always)
	public func apply(_ input: Input) throws(Failure) -> Output {
		try _initializer(input)
	}

	@inlinable @inline(__always)
	public func unapply(_ output: Output) -> Input {
		guard
			MemoryLayout<Input>.alignment == MemoryLayout<Output>.alignment,
			MemoryLayout<Input>.size == MemoryLayout<Output>.size
		else {
			fatalError()
		}
		return unsafeBitCast(output, to: Input.self)
	}
}

You will need both overloads, and also need to provide args as a constant, instead of a variable, unless you fix the compiler.

func call<each Arg, Result>(
  _ args: (repeat each Arg),
  _ f: (repeat each Arg) -> Result
) -> Result {
  f(repeat each args)
}

func call<each Arg, Result, Failure>(
  _ args: (repeat each Arg),
  _ f: (repeat each Arg) throws(Failure) -> Result
) throws(Failure) -> Result {
  try f(repeat each args)
}
func splat<each Arg, Result>(
  _ f: @escaping (repeat each Arg) -> Result
) -> ((repeat each Arg)) -> Result {
  { f(repeat each $0) }
}

func splat<each Arg, Result, Failure>(
  _ f: @escaping (repeat each Arg) throws(Failure) -> Result
) -> ((repeat each Arg)) throws(Failure) -> Result {
  { try f(repeat each $0) }
}
2 Likes

You will need both overloads, and also need to provide args as a constant, instead of a variable, unless you fix the compiler.

I had tried repeat each args in my larger project but since it was in a struct I thought it wasn't intended to work, thank you for confirming that it does! Means we don't need an extra operator or whatnot!

I didn't need both overload (if Failure == Never then call won't require try) but I did identify that the crash only occurs when a struct is involved.

func call<each Arg, Result, Failure>(
	_ f: (repeat each Arg) throws(Failure) -> Result,
	with args: (repeat each Arg)
) throws(Failure) -> Result {
	try f(repeat each args)
}

func test(x:Int,y:Int,z:Int) -> String {
	"\(x),\(y),\(z)"
}

print(call(test, with: (1,2,3)))

// if I add the following then swiftc crashes.

struct Executer<each Arg, Result, Failure: Error> {
	let task: (repeat each Arg) throws(Failure) -> Result

	func execute(with args: (repeat each Arg)) throws(Failure) {
		try call(task, with: args)
	}
}

print(Executer(task: test).execute(with: (2,3,5)))

I can't reproduce the problem now with call now. But it was happening as I was testing, and is sometimes a problem with other signatures, like that splat.

E.g. This crashes the compiler if you don't use the error-less overload.

func test(x: Int, y: Int, z: Int) -> String { "\(x),\(y),\(z)" }
_ = splat(test)((1, 2, 3))
1 Like

Don’t we have an according “problem” with variadic parameters vs. arrays, what would an analogous solution be, and should such solutions be part of Foundation? Or should the compiler be very smart instead (if possible)? I am not sure if a better option is to just duplicate the function and let one version call the other (as I usually do it).

2 Likes

I don't think anything better is or has ever been possible. So I just don't write anything with them.


You can make tuples out of sequences…

public extension Sequence {
  func tuplePrefix<each Element>() -> (repeat each Element) {
    var iterator = makeIterator()
    return (repeat iterator.next()! as! each Element)
  }
}
#expect((1...10).tuplePrefix() == (1, 2, 3)) // âś…

…and this will potentially get safer via Integer Generic Parameters.

But as far as I know, there's no way to pump expanded packs into variadic arguments…?

public func splat<Element, each TupleElement>(
  _ f: @escaping (Element...) -> Void
) -> ((repeat each TupleElement)) -> Void {
  // Error: Cannot pass value pack expansion to non-pack parameter of type 'Element'
  { return f(repeat each $0 as! Element) }
}

If that worked, you could do

splat(f)((1...10).tuplePrefix() as (Int, Int, Int))
1 Like

But as far as I know, there's no way to pump expanded packs into variadic arguments…

I’m not sure how variadic arguments are currently represented by Swift but I’d love if we could pass a sequence directly (like C#) so I wouldn’t have to double my overloads.

That and passing them as a Span rather than an Array for embedded!

For now I just never use them, I don’t think they’re worth the added overload maintenance.

1 Like