Working with text output streams

Here’s another great write-up from Tim V. 

By far the most common way to add a custom textual representation to a type is by implementing CustomStringConvertible. Consider this type:

struct Person {
    let name: String
    let age: Int
    let spouse: String?
    let children: [String]
}

A pretty standard way to conform to CustomStringConvertible would be this:

extension Person: CustomStringConvertible {
    var description: String {
        var description = ""
        
        description += "Name: \(name)\n"
        description += "Age: \(age)\n"
        
        if let spouse = spouse {
            description += "Spouse: \(spouse)\n"
        }
        
        if !children.isEmpty {
            description += "Children: \(children.joined(separator: ", "))\n"
        }
        
        return description
    }
}

This code doesn’t look too bad, but it’s a pain to write var description = "" and return description over and over, if this is a pattern you commonly use. It’s also quite easy to forget to add \n to each line.

The relatively unknown standard library protocol TextOutputStreamable solves both of these problems for you. Rather than adding a description computed property, all you have to do is write your properties to a TextOutputStream instance:

extension Person: TextOutputStreamable {
    func write<Target: TextOutputStream>(to target: inout Target) {
        print("Name:", name, to: &target)
        print("Age:", age, to: &target)
        
        if let spouse = spouse {
            print("Spouse:", spouse, to: &target)
        }
        
        if !children.isEmpty {
            print("Children:", children.joined(separator: ", "), to: &target)
        }
    }
}

That’s it! Whenever something that conforms to TextOutputStreamable but not to CustomStringConvertible is turned into a string, the write(to:) method we just implemented is used:

let person = Person(name: "Michael", age: 45, spouse: "Emma", children: ["Charlotte", "Jacob"])
print(person)

>Name: Michael
>Age: 45
>Spouse: Emma
>Children: Charlotte, Jacob

If you enjoyed this write-up, you might be interested in an old post I wrote about using streams to transform data.

6 Comments

  • Great tip!

    This may be a silly question, but isn’t write(to:) a generic function? If not, won’t we have to declare the type of Target somewhere?

    • Good catch, Chris! It wasn’t visible because it was interpreted as an HTML tag at first, but it’s fixed now.

      • Awesome, thanks! ✨

  • How, how are you doing this? I have almost understood everything :(((((

  • Check out my the talk: https://www.youtube.com/watch?v=kHG_zw75SjE for a more general treatment (1st half of the talk). The code is at https://github.com/mpw/MPWFoundation/tree/master/Streams.subproj Also discussed in the book: iOS and macOS Performance Tuning

  • The provided solution is not correct, since with multi-threaded output there is a chance that some other lines appear in between Name and Children, so it will be hard to tell when the instance description started and when it ended. There should be only one `print’ function call per instance.