How to call Swift's "print" function as closure

Default Swift "print" function has this signature

func print(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"
)

If I print the type of "print" function it outputs as:

print(type(of: print))
// (Any..., String, String) -> ()

For testing purposes, I want to pass this "print" function as an argument to a function so it will be called inside that function by using the parameter name rather than actually calling the "print" function itself.

func customPrint(printClosure: (Any..., String, String) -> (), text: String) {
    printClosure(text)
}

customPrint(printClosure: print, text: "Hello World")

printClosure parameter type is set to the exact type signature of Swift's default print function

printClosure: (Any..., String, String) -> (),

Yet on line of:

printClosure(text)

Swift gives error of

error: missing arguments for parameters #2, #3 in call

parameters of #2 separator and #3 terminator of "print" function are default so they do not need to be passed but swift requires to be passed here.

Because closures do not have argument labels during function call, it is not even possible to provide default values in printClosure() call.

printClosure(text, separator:" ", terminator:"\n") // does not work

How can we fix this code issue?

1 Like

You could say that printClosure has type (Any) -> (), and pass in { print($0) } as its value instead of print.

2 Likes

Thank you, it is a workaround but I want to go with the way I described.

Why I just can not use the default print function with its function signature? Is it a limitation?

It’s either a limitation or an intentional simplification, depending on your point of view :slight_smile:

In theory, we could allow this:

func f(_: Int, _: String = "") {}

let fn: (Int) -> () = f

This would amount to the type checker recognizing the conversion and wrapping the referenced function in a closure that applies the default argument. Conversely, by not allowing this conversion today, we don’t need to implement this logic.

5 Likes
func print(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"
)

This makes me wonder how the arguments are passed to the print function.

Are the arguments passed in reverse order? terminator first, separator second, and then the items, if any, also in reverse order?

I believe variadic args in Swift are packed into an Array object.

2 Likes

Yeah, T... is syntax sugar for an Array<T>. print(1, 2, 3) is sugar for print([1, 2, 3], separator: " ", terminator: "\n")

2 Likes

Notably this requires memory allocation, in contrast to C which just pushes parameters onto stack in reverse order.

Would be quite cool. Or this:

func f(a: Int, b: String) {}
let fn: (Int) -> () = f(b: "hello")
1 Like

C varargs are quite limited. The 'variadic' parameter must be the last one and the number of extra arguments is not actually encoded. I don't believe anything in the design requires that the arguments are passed in any specific order, or even that varargs are passed on the stack necessarily. The implementation just needs to provide a way to get a pointer to the first variadic argument, together with a mechanism to advance the pointer (with UB if you go too far).

4 Likes

If you want to use this for debugging purposes only, maybe it's better to use "OSLog" (iOS 14+)

I'd implement it differently to both, taking the best features of the two approaches:

  1. from C I'd take passing var-arg parameters on stack without creating an explicit array (or it could be treated as "creating array on stack without using memory allocation").

  2. from Swift I'd take the full or minimal "array semantics", so you'd be able getting "count" of var-arg parameter, subscript it, and possibly use it in a for-each loop. There won't be UB when accessing the varargs elements out of bounds, it will safely trap similar to how array subscript does. It doesn't have to be a proper array, e.g. this could result into error: "var array: [Int] = varArgParameter"; making it a normal array is IMHO an anti goal.

Perhaps the easiest way of implementing it is to pass an additional invisible "count" parameter after pushing varargs parameters on stack.

Not a huge deal IMHO. And quite possible to implement without necessarily using normal (dynamically allocated) arrays for var-arg parameters.

Once we get same type constraints on parameter packs this all becomes a moot point, because variadic generics are indesputably a better solution than either approach.

1 Like

So long as passing variadic generics do not cause locks or memory allocations.

2 Likes

Directly passing varargs as multiple arguments in the calling convention is actually an unnecessary complication these days. Unlike classic C, the calling convention for int(int, int) is not actually the same as int(…) for example, because ordinary arguments are passed in registers. It’s better to pass a pointer to a stack-allocated array of values, together with the count. That’s in fact how parameter packs work, however they do have an extra level of indirection where a value pack is an array of pointers; it doesn’t store the values inline. So to pass a pack of Ints, we need to materialize the Ints on the stack, then we form another stack-allocated array of pointers to those values, and we finally pass this array with a count to the callee.

An escaping closure that captures a type pack will call into the runtime to heap-allocate and unique the type pack (for each combination of types). Also if you do something unspecialized generic with a variadic nominal type, it will instantiate the type pack. Most other type packs and all value packs are passed on the stack.

Once we have same-element requirements, the representation will be that a type pack that is same-element constrained doesn’t need to be passed at all, just like <T where T == Int> doesn’t need runtime type metadata for T.

1 Like