TextOutputStream
print
is among the most-used functions in the Swift standard library.
Indeed, it’s the first function a programmer learns
when writing “Hello, world!”.
So it’s surprising how few of us are familiar with its other forms.
For instance,
did you know that the actual signature of print
is
print(_:separator:terminator:)
?
Or that it had a variant named
print(_:separator:terminator:to:)
?
Shocking, I know.
It’s like learning that your best friend “Chaz” goes by his middle name and that his full legal name is actually “R. Buckminster Charles Lagrand Jr.” — oh, and also, they’ve had an identical twin the whole time.
Once you’ve taken a moment to collect yourself, read on to find out the whole truth about a function that you may have previously thought to need no further introduction.
Let’s start by taking a closer look at that function declaration from before:
func print<Target>(_ items: Any...,
separator: String = default,
terminator: String = default,
to output: inout Target)
where Target : Text Output Stream
This overload of print
takes a variable-length list of arguments,
followed by separator
and terminator
parameters —
both of which have default values.
-
separator
is the string used to join the representation of each element initems
into a single string. By default, this is a space (" "
). -
terminator
is the string appended to the end of the printed representation. By default, this is a newline ("\n"
).
The last parameter, output
takes a mutable instance of a generic Target
type
that conforms to the Text
protocol.
An instance of a type conforming to Text
can be passed to the print(_:to:)
function
to capture and redirect strings from standard output.
Implementing a Custom Text Output Stream Type
Due to the mercurial nature of Unicode, you can’t know what characters lurk within a string just by looking at it. Between combining marks, format characters, unsupported characters, variation sequences, ligatures, digraphs, and other presentational forms, a single extended grapheme cluster can contain much more than meets the eye.
So as an example,
let’s create a custom type that conforms to Text
.
Instead of writing a string to standard output verbatim,
we’ll have it inspect each constituent code point.
Conforming to the Text
protocol is simply a matter of
fulfilling the write(_:)
method requirement.
protocol Text Output Stream {
mutating func write(_ string: String)
}
In our implementation,
we iterate over each Unicode.Scalar
value in the passed string;
the enumerated()
collection method
provides the current offset on each loop.
At the top of the method,
a guard
statement bails out early if the string is empty or a newline
(this reduces the amount of noise in the console).
struct Unicode Logger: Text Output Stream {
mutating func write(_ string: String) {
guard !string.is Empty && string != "\n" else {
return
}
for (index, unicode Scalar) in
string.unicode Scalars.lazy.enumerated()
{
let name = unicode Scalar.name ?? ""
let code Point = String(format: "U+%04X", unicode Scalar.value)
print("\(index): \(unicode Scalar) \(code Point)\t\(name)")
}
}
}
To use our new Unicode
type,
initialize it and assign it to a variable (with var
)
so that it can be passed as an inout
argument.
Anytime we want to get an X-ray of a string
instead of merely printing its surface representation,
we can tack on an additional parameter to our print
statement.
Doing so allows us to reveal a secret about the emoji character 👨👩👧👧: it’s actually a sequence of four individual emoji joined by ZWJ characters — seven code points in total!
print("👨👩👧👧")
// Prints: "👨👩👧👧"
var logger = Unicode Logger()
print("👨👩👧👧", to: &logger)
// Prints:
// 0: 👨 U+1F468 MAN
// 1: U+200D ZERO WIDTH JOINER
// 2: 👩 U+1F469 WOMAN
// 3: U+200D ZERO WIDTH JOINER
// 4: 👧 U+1F467 GIRL
// 5: U+200D ZERO WIDTH JOINER
// 6: 👧 U+1F467 GIRL
Ideas for Using Custom Text Output Streams
Now that we know about an obscure part of the Swift standard library, what can we do with it?
As it turns out,
there are plenty of potential use cases for Text
.
To get a better sense of what they are,
consider the following examples:
Logging to Standard Error
By default,
Swift print
statements are directed to
standard output (stdout
).
If you wanted to instead direct to
standard error (stderr
),
you could create a new text output stream type
and use it in the following way:
import func Darwin.fputs
import var Darwin.stderr
struct Stderr Output Stream: Text Output Stream {
mutating func write(_ string: String) {
fputs(string, stderr)
}
}
var standard Error = Stderr Output Stream()
print("Error!", to: &standard Error)
Writing Output to a File
The previous example of writing to stderr
can be generalized to write to any stream or file
by instead creating an output stream to a File
(for which standard error is accessible through a type property).
import Foundation
struct File Handler Output Stream: Text Output Stream {
private let file Handle: File Handle
let encoding: String.Encoding
init(_ file Handle: File Handle, encoding: String.Encoding = .utf8) {
self.file Handle = file Handle
self.encoding = encoding
}
mutating func write(_ string: String) {
if let data = string.data(using: encoding) {
file Handle.write(data)
}
}
}
Following this approach,
you can customize print
to write to a file instead of a stream.
let url = URL(file URLWith Path: "/path/to/file.txt")
let file Handle = try File Handle(for Writing To: url)
var output = File Handler Output Stream(file Handle)
print("\(Date())", to: &output)
Escaping Streamed Output
As a final example,
let’s imagine a situation in which you find yourself
frequently copy-pasting console output into a form on some website.
Unfortunately,
the website has the unhelpful behavior of
trying to parse <
and >
as if they were HTML.
Rather than taking an extra step to escape the text
each time you post to the site,
you could create a Text
that takes care of that for you automatically
(in this case, we use an XML-escaping function
that we found buried deep in Core Foundation).
import Foundation
struct XMLEscaping Logger: Text Output Stream {
mutating func write(_ string: String) {
guard !string.is Empty && string != "\n",
let xml Escaped = CFXMLCreate String By Escaping Entities(nil, string as NSString, nil)
else {
return
}
print(xml Escaped)
}
}
var logger = XMLEscaping Logger()
print("<3", to: &logger)
// Prints "<3"
Printing is a familiar and convenient way for developers to understand the behavior of their code. It complements more comprehensive techniques like logging frameworks and debuggers, and — in the case of Swift — proves to be quite capable in its own right.
Have any other cool ideas for using Text
that you’d like to share?
Let us know on Twitter!