Higher order function using variadic parameters

I have a C library with a uniform API. Using it in swift, every function call would roughly look like this:

let error = FooError()
let res = foo_func(arg1, arg2, &error)

if (foo_is_error(&error)) {
    return .failure(SwiftFooError(&error))
}

return .success(res)

This becomes quite verbose. Coming from C++ I thought I could write a function akin to:

template<typename F, typename... Args>
auto func_to_result(F&& func, Args&&... args) -> Result<std::invoke_result_t<F, Args...>, FooError> {
    FooError err;
    auto res = std::invoke(std::forward<F>(func), std::forward<Args>(args)..., &err);

    if (foo_is_error(&err)) {
         return Result::error(err);
    }

    return Result::success(res);
}

My latest attempt looks as follows:

func funcToResult<T, each Args>(
    _ function: (repeat each Args, UnsafeMutablePointer<FooError>) -> T,
    args: repeat each Args
) -> Result<T, SwiftFooError> {
    var error = FooError()

    let result = function(repeat each args, &error)

    if foo_is_error(&error) {
        return .failure(SwiftFooError.init(error))
    }

    return .success(result)
}

I also tried standard parameter packs (Any...) and taking in an array ([Any]) for the parameters. None of the approaches work.

From my beginner's perspective, the errors appear to be as follows:

  1. with repeat each args: Swift cannot call a non-repeat-each function with repeat-each arguments
  2. with Any...: Swift doesn't manage to differentiate between the Error parameter and the Any... parameters
  3. with [Any]: The function signature at the callsite doesn't match anymore

How would I go about writing this helper? Perhaps a macro is in order?

1 Like

Functions will generate relevant errors. Closures will not (yet).

// Error: A parameter following a variadic parameter requires a label
func f<each T>(t: repeat each T, _: Void) { }

I don't understand why this is required, when only one pack is used. Perhaps someone can explain it, as I don't think the proposal does.


Two choices:

  1. Tupleize the arguments.
func funcToResult<T, each Arg>(
  _ function: ((repeat each Arg), UnsafeMutablePointer<FooError>) -> T,
  _ arg: repeat each Arg
) throws(SwiftFooError) -> T {
  var error = FooError()
  let result = function((repeat each arg), &error)
  guard !foo_is_error(&error) else { throw .init(error) }
  return result
}
  1. Reverse the closure's parameters.
func funcToResult<T, each Arg>(
  _ function: (UnsafeMutablePointer<FooError>, repeat each Arg) -> T,
  _ arg: repeat each Arg
) throws(SwiftFooError) -> T {
  var error = FooError()
  let result = function(&error, repeat each arg)
  guard !foo_is_error(&error) else { throw .init(error) }
  return result
}
1 Like

How would the compiler be able to tell that a parameter is not part of the pack if there's no label for the next parameter?

I don't see how there's any ambiguity if packs are completely surrounded by unlabeled non-packs. Do you have an illuminating example?


And is there any reason this should compile? (It does compile but I don't think it's callable.)

func f<each Arg>(
  _: (repeat each Arg, repeat each Arg) -> Void
) { }

Except for trailing closures (and this is fixed in Swift 6), Swift processes arguments left to right. If Swift allowed a function to be declared like so:

func f<each Arg>(_ first: Int, _ arg: repeat each Arg, _ last: Int) { ... }

Then this is how a call to it would resolve:

f(
    42, // first
    "Foo", false, 98.6, ["Hello", "World"], // arg
    69 // whoops, still arg
) // error: missing parameter

And there'd be no way to fix the error, since the missing parameter doesn't have a label.

I have no idea whether your final example should compile nor if there's a way to call it.

Oh, I didn't mean ambiguity to the compiler. I meant actual ambiguity.

The situation we have now, where the first of these compiles, and the second does not, will lead to more threads like this one. I don't think it's right for either to be allowed if the other is not.

func f<each T>(_: Void, _: repeat each T) { }
func f<each T>(_: repeat each T, _: Void) { }

This is a problem with variadic parameters, too. I don't think they've proven themselves to be nearly as important, but also should be fixed.

I don't see this as a problem, and there's definitely cases where a non-optional first parameter followed by a optional list of other parameters is useful. And once same type requirements on parameter packs are implemented variadic parameters can be replaced by parameter packs, and then we don't have to deal with the splatting problem anymore.

It would be nice if we could allow that second kind of function to compile, but removing the first kind because we can't is not a useful consistency.

As demonstrated by

(repeat each Args, UnsafeMutablePointer<FooError>) -> T

above, all of the relevant possibilities are potentially useful. Forcing argument position because of this left-to-right business is programmer-unfriendly at best. We don't even need default arguments for the problems to arise anymore, as with the pre-parameter pack days…


f("")

works fine with either of these:

func f<T>(_: T, _: Int = 0) { }
func f<each T>(_: repeat each T, labeled: Int = 0) { }

…but with either of these…

func f<T>(_: Int = 0, _: T) { }
func f<each T>(_: Int = 0, _: repeat each T) { }

Cannot convert value of type 'String' to expected argument type 'Int'