Trying to get a handle on function signatures

Hi folks,

I'm new to Swift and trying to get a better understanding of function signatures, especially as relates to passing them to other functions. For example:

func sayHi() -> Void { // Simplest version of signature for a thunk
    print("hi")
}
func sayArg(_ arg: Any) {  // It's legal to not give a return type when not needed
    print(arg)
}
func sayOptionalArg(_ arg: Any = "no arg given") {
    print(arg)
}
func sayArgWithKeyword(arg: Any)  {  // non-positional argument
    print(arg)
}

//  These all work as expected, including the fact that 'sayArg' doesn't care about the
//  data type of its argument
sayHi()
sayArg(17)
sayArg("hola")
sayOptionalArg()
sayOptionalArg("optional arg!")
sayArgWithKeyword(arg: "foo")

//----------------------------------------------------------------------

//  Let's try passing and executing closures
func runThunk(_ thunk: () -> ()) {
    thunk()
}

//  This works as expected
runThunk( sayHi )


// As expected, this has a compiler error. Reason: sayArg has a mandatory argument
//runThunk( sayArg )  


func runOneArg (closure: (Any) -> Any)  { 
    closure(17)
}

// Question: how can I silence the "result of call [...] is unused" warning?
runOneArg(closure: sayArg)

//----------------------------------------------------------------------

//  How about some bog-standard functional programming?  A function that takes two
//  arguments but at point of use I have the arguments in a tuple and I want to
//  automatically unpack them
func addTwo(_ a: Int, _ b: Int) -> Int {
    a + b
}
print(addTwo(7, 8)) // works as expected when the arguments are passed individually

//  Question: How do I do this pseudocode for real? 
//     Pseudocode:  apply(addTwo, [7,8])  // Should equate to addTwo(7,8)
//
//  Obviously I could dig the arguments out manually but the point of `apply` is to
//  generalize the process of combining functions and arguments, especially variadic
//  functions.

//----------------------------------------------------------------------

// Question: Suppose I want a function that:
//    - takes two arguments
//    - the arguments are:
//        * a function that requires 2 or more arguments
//        * a list of whatever
//    - unwraps the arguments list and runs the provided function with the contents
//    - returns whatever
//
// In Racket this would be:
//
// (define/contract (foo func args)
//   ((procedure-arity-at-least/c 2) list? . -> . any)
//   (apply func args))


//  In Swift this would be...?
func runVariadicButTwoOrMoreArgs(closure: ???, args: ???) -> Any {
    ??
}

Any help much appreciated.

You can explicitly ignore the return value by assigning it to _:

func runOneArg (closure: (Any) -> Any)  { 
    _ = closure(17)
}

You can write this as a generic variadic function:

func runVariadic<each T, R>(closure: (repeat each T) -> R, args: (repeat each T)) -> R {
  return closure(repeat each args)
}

let x = runVariadic(closure: add2, args: (7, 8))
3 Likes

Ah, of course. I should have realized. Thanks.

Oh, cool. I hadn't run into generics yet.

goes and reads up on them

If I'm reading this right, that signature says:

  • create the function runVariadic
  • it takes two arguments
  • the first is a function of 0 or more arguments, all of which must be of the same type
  • the second is a tuple of 0 or more items, all of which must be of the same type
  • it returns something of whatever type

Do I have that right? If so, that's good to have but not what I was originally asking about. I was trying to find something that required the passed-in function to take at least 2 arguments and for the list to contain whatever you like, not necessarily items of the same type.

Also, how is Swift determining the value for R (i.e., the return type of runVariadic)?

Not quite. A variadic type parameter each T does allow for 0 or more arguments, but the arguments are allowed to be different from each other. Since the same type variable each T is used in the signature for closure's parameters and for the tuple elements of args, the requirement is that you pass the same number of values of the same type for the args tuple as closure takes arguments. If you want to require at least two arguments, you can specify that by adding a couple more parameters outside the pack:

func runVariadic<T0, T1, each TN, R>(closure: (T0, T1, repeat each TN) -> R, args: (T0, T1, repeat each TN)) -> R {
  return closure(repeat each args)
}

which sounds like what you were asking for originally.

It would be inferred from the value of closure you pass, so that the call to runVariadic returns the same type as the closure returns.

3 Likes

Aha. Thank you, that helps. Could I do this?

func runVariadic<T0, T1, each T, U0, U1, each U, R>(closure: (T0, T1, repeat each T) -> R, args: (U0, U1, repeat each U)) -> R {
  return closure(repeat each args)
}

If I've got this right, it means:

  • Create func runVariadic
  • It takes two arguments
  • The first is a function of 2 or more arguments. The types of the arguments are not constrained and could be different from one another
  • The second is a tuple of 2 or more values. The types of the values are not constrained and could be different from one another.
  • The number of parameters and the number of arguments might not be the same, although both will be at least 2.

The idea here is that you might pass in a variadic function so there isn't a constraint on passing more arguments than the function consumes. Although perhaps that is anti-Swift since it's allowing the programmer to shoot themselves in the foot cause a runtime error if they pass in incompatible arguments? I'm still getting a handle on where Swift falls on the permissive vs B&D axis.

1 Like

Swift will generally want the types of things to match in order to let you call them. You could have a function like you described, but you'd need to do a cast to check whether the types line up, like:

func runVariadic<T0, T1, each T, U0, U1, each U, R>(closure: (T0, T1, repeat each T) -> R, args: (U0, U1, repeat each U)) -> R {
  let checkedArgs = args as! (T0, T1, repeat each T)
  return closure(repeat each checkedArgs)
}

as! will runtime trap if the types don't match up, as you noted. You could use as? instead, and pass up an Optional or throw an error if the types don't align, and you want the caller to be able to react to that:

struct BadCallError: Error {}

func runVariadic<T0, T1, each T, U0, U1, each U, R>(closure: (T0, T1, repeat each T) -> R, args: (U0, U1, repeat each U)) throws -> R {
  guard let checkedArgs = args as? (T0, T1, repeat each T) else {
    throw BadCallError()
  }
  return closure(repeat each checkedArgs)
}
2 Likes