[Pitch] - Variadic `print` function

Now that we have variadic functions in Swift, we should provide a version of the print function that takes a variadic parameter.

public func print<each Item>(
  _ item: repeat each Item,
  separator: String = " ",
  terminator: String = "\n"
) { ... }

public func print<each Item, Target: TextOutputStream>(
  _ item: repeat each Item,
  separator: String = " ",
  terminator: String = "\n",
  to target: inout Target
) { ... }

We could also provide a fast path for when all of the items conform to CustomStringConvertible.

public func print<each Item: CustomStringConvertible>(...) { ... }

For consistency we might also want to add variadic versions of debugPrint and dump.

Implementation

@_disfavoredOverload
@inlinable
public func print<each Item>(
  _ item: repeat each Item,
  separator: String = " ",
  terminator: String = "\n"
) { 
  var strings = [String]()
  repeat strings.append(String(describing: each item))
  print(strings.joined(separator: separator), terminator: terminator)
}

public func print<each Item, Target: TextOutputStream>(
  _ item: repeat each Item,
  separator: String = " ",
  terminator: String = "\n",
  to target: inout Target
) { 
  var strings = [String]()
  repeat strings.append(String(describing: each item))
  print(strings.joined(separator: separator), terminator: terminator, to: &target)
}
5 Likes

Isn’t print already variadic?

1 Like

I like the idea of a print function that doesn’t need three casts inside it to print something, but I’m unclear on what the point is of the non-constrained version? I guess maybe it doesn’t need to box its inputs into Any?

It also seems like it would be really easy to accidentally add a parameter that doesn’t conform and fall out of the fast-path case without noticing.

The point would be if someone is trying to print a variadic argument to a function. Using the current print function, you get this error.
Cannot pass value pack expansion to non-pack parameter of type 'Any'
It's possible it could be made more efficient using variadic generics, but that's not really the point.

But also, I think we only made the signature use a variadic Any parameter because we didn't have variadic generics. Now that we do, why not change it? There's no real reason print needs to type-erase its arguments.

2 Likes

This is unrelated, but I think there should also be a similar function added to DefaultStringInterpolation.

extension DefaultStringInterpolation {
  @_disfavoredOverload
  public mutating func appendInterpolation<each Segment>(_ segment: repeat each Segment) {
    for segment in repeat each segment {
      self.appendInterpolation(segment)
    }
  }
}
3 Likes

It’d be nice if we could have a variadic print that accepts just CustomStringConvertible, CustomDebugStringConvertible, TextOutputStreamable, and (eventually) Reflectable. These are already cast in the underlying print function. However, we might be able to use property wrappers to statically check:

func print<each Item>(
  @PrintItem _ items: repeat each Item
) {}

@propertyWrapper struct PrintItem<Value> {
  let value: Value

  init(
    wrappedValue: Value
  ) where Value: CustomStringConvertible { … }

  // Repeat init for other protocols...

  @available(*, unavailable, message: “This value is not CustomStringConvertible, or …”)
  init(wrappedValue: Value) {}
}

Admittedly, we might need to help the compiler resolve the right overload when a type conforms to more than one type, but I think that’s still a good starting step.

Well that's basically a protocol OR constraint, which apparently the type system 'cannot and should not support'. I do wish it could though. One way to get around this limitation would be for the standard library to make new protocols for printing, and then make the existing string conversion protocols refinements of that protocol.

public protocol _CustomPrintable {
  var _printingRepresentation: String { get }
}

protocol CustomStringConvertible: _CustomPrintable { 
  var description: String { get }
}

extension CustomStringConvertible {
  var _printingRepresentation: String { description }
}

/* etc for other protocols */
1 Like

But also, this property wrapper would still require dynamic casting to a protocol type, even if it is statically known that the value conforms to one of the printing protocols.

Yeah, I forgot to clarify that this would help with opt-in reflection metadata through Reflectable. Because if a user prints a non-Reflectable value, it would just output “value cannot be printed without reflection”. It’d be better to get a static warning or error telling the user that at compile time.

If printing relies on each item being reflectable, why not just do this?

func print<each Item: Reflectable>(_ item: repeat each Item, separator: String, terminator: String)

Note that you can call print(...) inside of a pack expansion to print each element in a pack, e.g.

func printEach<each T>(_ t: repeat each T) {
  repeat print(each t)
}

Yes, but sometimes that isn't what you want. That prints each element of the pack on a newline, compared to how the print function usually works. You would expect:

print(1, 2, 3) // prints 1 2 3

But if you do the same thing with a tuple

func printTuple<each T>(_ t: repeat each T) {
  repeat print(each t)
}

printTuple(1, 2, 3) 
/*
prints:
1
2
3
*/
1 Like

What about this then?

func printTuple<each T>(_ t: repeat each T, separator: String = " ", terminator: String = "/n") {
    repeat print(each t, terminator: separator)
    print(terminator, terminator: "")
}

That's possible, but non-obvious; why not provide this functionality in the standard library?

Honestly, I'm not sure it's worth the effort to make a variadic version of print unless we go all the way to something like fmtlib.

This implementation is a bit wrong. printTuple(42) will output 42<SPACE>, not 42.

I'm using this instead;

func print<First, each T>(_ firstValue: First, _ value: repeat each T, separator: String = " ", terminator: String = "\n") {
    func printWithPrefix<U>(value: U) {
        print(separator, terminator: "")
        print(value, terminator: "")
    }
    print(firstValue, terminator: "")
    repeat printWithPrefixSpace(value: each value)
    print(terminator, terminator: "")
}

Anyway, print(repeat each value) will eventually be supported. (Ref)