Swift Developer. Living and Xcoding in Brooklyn NY.

Blog

Rise of the Bots: GCD with a touch of functional swift

It seems we're in the midst of a trend toward bot-enabled services and applications. Indeed, there's a growing number of applications that revolve around the ability to interact with a human-like operator via a text-based chat interface (e.g. Lark, Operator). I am in the camp that thinks the ultimate interface is no interface at all, and voice-based assistants such as Apple's Siri and Microsoft's Cortana are taking us toward that future, but I digress ... Let's get back to the topic at hand, shall we?

The Problem

It so happens one of my current projects involves a chatbot interface, and an interesting challenge we encountered was that the response times of our bot were too fast. Messages were being sent to and from the server so quickly that our beta testers complained the interaction felt too unnatural! So we had to engineer an artificial delay that felt a little more like talking to a real person.

Our specific problem was that certain responses were received as an array, but had to be staggered one after another and presented to the user sequentially. "What's that?" you say? "Easy-peasy - loop over the array and throw in a little Grand Central Dispatch magic! Piece of cake, right?" Not so fast, cowgirls and cowboys. You'd think the following piece of code would do the trick, right? Riiiiight?


   public func printLoop() {
        print("\nDISPATCH AFTER\n ")
        for response in responses  {
            let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64(2.0 * Double(NSEC_PER_SEC)))
            dispatch_after(popTime, dispatch_get_main_queue(), { [weak self] in
                self?.showResponse(response)
                })
        }
    }

Turns out the above snippet results in only a single initial delay after which our messages are presented to the user simultaneously anyway!

#@$&! (a.k.a "What gives?")

After some digging, this Stack Overflow post helped clarify some fundamentals of how dispatch_after works, namely:

The dispatch_after(...) call returns immediately no matter when it is scheduled to run. This means that your loop is not waiting two seconds between dispatching them. Instead you are building an infinite queue of things that will happen two seconds from now, not two seconds between each other.
- Stack Overflow user David Rönnqvist

All the dispatch call was doing was queueing up tasks on the given (serial) queue and returning immediately. Therefore, the DISPATCH_TIME_NOW for each task changed so little between iterations so as to be imperceptible to the user (and certainly won't register on a standard timestamp interval).

The Fix 

Instead of throwing in the towel and pursuing another approach (such as nesting dispatch_after calls), one of my peers suggested we instead modify our code to use Swift's handy-dandy enumerate function so that each message's dispatch time is based on its position in the array:


     public func printLoopEnumerated() {
        for (index,response) in responses.enumerate() {
            let shift   = Double(2.0)
            let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64((shift + (shift * Double(index))) * Double(NSEC_PER_SEC)))
            dispatch_after(popTime, dispatch_get_main_queue(), { [weak self] in
                if index == 0 {
                    print("\nDISPATCH AFTER WITH ENUMERATION\n")
                }
                self?.showResponse(response)
                })
        }
    }

To help confirm the point, I crafted a quick Xcode playground project that dispatched messages to the console. As you can see by the corresponding timestamps, the dispatch call using an enumerated dispatch_time works as we would like it to.

Pretty neat, huh?

Click here to check out the playground on Github. We're still a ways out from crafting our own android femme fatale. But we're one step closer. 😁

(And by the way, if you haven't seen Ex-Machina, it's a brilliant film and please go watch it meow. 🐈)