Swift’s Limited Oberservers and How To Fix Them

Last week I was tearing apart code generated by the Swift 3.1 compiler to look at how the Swift observers, willSet, and didSet are implemented. I was surprised by a number of things, mostly by how limited the observers are.

Let’s start with the background. Swift variables are, in actuality, a lot closer to .NET properties. When you declare the following class:

open class Sample {
   public var x:Int = 0
}

The swift compiler will reserve space in the class for x, but it will also author two hidden methods, a getter for x and a setter for x. Inside the class, x will act like a field, but outside the class the compiler may choose to use the hidden methods instead.

Now let’s add in willSet and didSet methods:

open class Sample {
   public var x:Int = 0 {
       willSet {
          print("Sample x will be set to \(newValue)")
       }
       didSet {
          print("Sample x changed, old value is \(oldValue)")
       }
   }
}

Now if we make and instance of Sample and set it’s x to, say, 7, this will print “Sample x will be set to 7” and then “Sample x changed, old value is 0”.

There are some limitations though. In Swift, there are a few different types of properties: stored properties and computed properties. In these examples, x is a stored property. If x was computed property, we’d have problems.

open class Sample {
   public var w:Int = 0
   public var x:Int {
       get {
           return w * 2
       }
       set {
           w = newValue / 2
       }
       willSet { // compiler error
          print("Sample x will be set to \(newValue)")
       }
       didSet { // compiler error
          print("Sample x changed, old value is \(oldValue)")
       }
   }
}

Swift hates when you put observers on computed properties. Unless they’re overrides.

open class Sample {
   public var w:Int = 0
   public var x:Int {
       get {
           return w * 2
       }
       set {
           w = newValue / 2
       }
   }
}
open class SubSample : Sample {
    override var x:Int {
       willSet { // legal
          print("SubSample x will be set to \(newValue)")
       }
       didSet { // legal
          print("SubSample x changed, old value is \(oldValue)")
       }
   }
}

The reason this is the case is in the Swift language reference:

The stored or computed nature of an inherited property is not known by a subclass

And with all this information, we can get a clear picture at how this is implemented.  Here’s a more complete example with output:

open class Sample {
   public var x:Int = 0 {
      willSet {
         print("Sample willSet")
      }
      didSet {
         print("Sample didSet")
      }
   }
}
open class SubSample : Sample {
    override var x:Int {
       willSet {
          print("SubSample willSet")
       }
       didSet { // legal
          print("SubSample didSet")
       }
   }
}
let cl = SubSample()
cl.x = 7
// output:
// SubSample willSet
// Sample willSet
// Sample didSet
// SubSample didSet

Note that the parent’s willSet/didSet pair are nested inside the child’s. This has to do with the implementation of the setters. Each setter looks syntactically like a field set, but there’s more. It ends up looking like this:

open class Sample {
   public var x:Int = 0 {
      set (newValue) {
          let oldValue = x_field
          willSet(newValue)
          x_field = newValue
          didSet(oldValue)
      }
      willSet {
         print("Sample willSet")
      }
      didSet {
         print("Sample didSet")
      }
   }
}
open class SubSample : Sample {
    override var x:Int {
      set (newValue) {
          let oldValue = super.x
          willSet(newValue)
          super.x = newValue
          didSet(oldValue)
      }
      willSet {
          print("SubSample willSet")
      }
      didSet { // legal
          print("SubSample didSet")
      }
   }
}
 

What happens is that if you don’t supply a setter, the swift compiler will inject the willSet/didSet code for you. And that explains why you don’t get this code in computed properties. The compiler would have to inject the willSet/didSet as a prelude/postlude, but it starts to get complicated especially when the setter has multiple exit points or unusual flow. They punted rather than generate unpredictable code and I don’t fault them for it.

But there’s a problem with the observers as is. They’re just not good for very much, honestly. I’d call them syntactic sugar, but they’re not because they don’t really sweeten things. This is more like Java where if you want to do something actually useful, you end up typing in a pile of boiler plate code (violating D.R.Y.). Let’s call it syntactic coffee (I dislike coffee, it’s nasty and bitter). Here’s what you’d really like an observer to do: have a list of clients that subscribe to it and get notified when the code reaches the point of inflection. Instead, Swift hides the point of inflections and only objects in the hierarchy have access to them. If you want more, you have to do a lot more work and at that point why are you even using willSet/didSet?

To be fair, the .NET event pattern (which is a single broadcaster, multiple listener model) is nearly as bad in terms of repetition, but at least the use of EventHandler<T> has made that better, compared with what it used to be.

Can we make this better? Somewhat.

public class Broadcaster {
    private var _currentTag:Int = 0
    private var _listeners:[(_listener:(T)->(), _tag:Int)] = []
    public init() { }
 
    // represents a synchronous lock
    private func lock( _ lock: Any, closure: () -> ()) {
        objc_sync_enter(lock)
        closure()
        objc_sync_exit(lock)
    }
 
    // add a listener to the chain of listeners
    public func add(listener:@escaping (T)->()) -> Int
    {
        var tag = 0
        lock (self) {
            tag = _currentTag;
            _currentTag = _currentTag + 1
            _listeners.append((listener, tag))
        }
        return tag;
    }
 
    private func indexOf(tag: Int) -> Int? {
        return _listeners.index(where: { (l, t) in return t == tag })
    }
 
    // remove a listener associated with tag.
    // returns true if it was found and removed
 
    public func remove(tag: Int) -> Bool
    {
        var index:Int? = nil
        lock (self) {
            index = indexOf(tag:tag)
            if index != nil {
                _listeners.remove(at: index!)
            }
        }
        return index != nil
    }
 
    // returns true if there is a listener associated with the given tag
    public func contains(tag:Int) -> Bool {
        var containsIt = false
        lock (self) {
            containsIt = indexOf(tag:tag) != nil
        }
        return containsIt
    }
 
    // returns a listner for the given tag, nil is not found
    public func listenerFor(tag: Int) -> ((T)->())? {
        var listener: ((T)->())? = nil
        lock (self) {
            if let index = indexOf(tag:tag) {
                listener = _listeners[index]._listener
            }
        }
        return listener
    }
 
    // broadcast the value to each listener
    public func broadcast(value:T) {
        lock (self) {
            _listeners.forEach() { listener in listener._listener(value) }
        }
    }
 
    // sugar operator
    static func <= (left: Broadcaster, right: @escaping (T)->()) -> Int {
        return left.add(listener: right)
    }
}
 
public class Sample {
   public var willSetX = Broadcaster()
   public var didSetX = Broadcaster()
   public var x:Int {
       willSet {
           willSetX.broadcast(newValue)
       }
       didSet {
           didSetX.broadcast(newValue)
       }
   }
}
let cl = Sample()
cl.willSetX <= { newValue in print("outside listener heard a new value \(newValue)"); }
cl.x = 5
// output
// outside listener heard a new value 5

My Broadcaster class is a little more than is needed in most cases, but the thread safety is important, especially in apps where the UI is running on the main thread and broadcasting changes to other threads. I did this with the lock function. Swift has a cute syntactic hack that if you write a function whose last argument is a closure you can either pass the closure in as an argument or put the closure afterwards. Since closure syntax was designed to always be in braces, it makes it look like you’re adding new syntax to the language. In fact, you’re not and the abstraction here is leaky as all hell and bit me a couple times while I was using it. For example, in the add() method, I wanted to return the tag inside the closure, but that makes no sense since the closure type returns an empty tuple (aka, void), so I had to put in a state variable. There’s not much you can do to fix this.

But we see that with this little gem, we can at least at proper even handling to Swift even if it still has a somewhat bitter syntactic coffee flavor.

I’m Old, Part LXIV: Pre Web Hacking

Around about 1993, Mattel released Teen Talk Barbie, a doll with a voice box that came programmed with 4 phrases (out of a suite of 270). Most wee innocuous, but some were vapid and the notorious “Math class is tough” raised a stink. So much that a group named the BLO (Barbie Liberation Organization) made a video about how they swapped the voice boxes in these Barbies and G.I.Joe equivalents and returned them to the shelves.

Alan Wootton heard about this and decided it was a great idea and set out to recreate it. He bought a talking G. I. Joe and spent some time trying to disassemble the doll. The first problem he encountered was that these kids of dolls are not made to be taken apart. These companies would suffer badly if a kid took one apart and choked on pieces. Alan mounted an X-Acto blade onto a soldering iron and used that to cut open the doll and get out the circuitry. it wasn’t going to be as simple as swapping chips – one doll was made by Mattel, the other by Hasbro. The likelihood of a compatible chipset was about none. The other choice was to swap the whole circuit boards, but again, the form factor for G. I. Joe’s chest and Babries weren’t even close.

Still, the G. I. Joe circuit was pretty nifty. Rather than have a few stock sentences, it had a dozen or so phrases forms and many pieces that could be substituted in. It was a much more interesting toy than teen Barbie.

Alan gave up on the process, being pretty sure that the BLO work was either just a joke or a pretty bad hack job. Alan reassembled the G. I. Joe and used the same rig he used to cut it open, to weld it back together. He ended up giving it to one of his nephews who, if I recall correctly, enjoyed the battle scars.

I’m Old, Part LXIII: Oh We Like Bacon

I really liked the time that I spent at Atalasoft. Not only was I doing work that I really liked, I felt like any employee had the ability to make the workplace better on any given day.

One of the conferences that we attended every year was AIIM, which is dedicated to document management and imaging for the most part. I have worked the floor of that show a couple times, but after we had filled out our sales staff more, we didn’t have to dig into engineering for running the booth.

The result was that during AIIM, our office was missing close to 1/3 of the people who were working long days and then hitting the post show bar with customers. Damn them. So on the Monday of AIIM week, I was talking to Christina Gay about what we should do and we decided that we would have bacon day. I knew a company, Burger’s Smokehouse, that sells truly excellent bacon and ships promptly and reliably. We ordered some bacon and arranged with the remaining staff to have some eggs and other things brought in. Christina volunteered her griddle for cooking.

We showed up a little early, cooked up piles of bacon and eggs and enjoyed a really nice change of pace. The office was in an old mill building and featured holes in the floor that allowed us to see in the offices below and consequently the scent of cooking bacon spread. One of the other offices tweeted at us about the smell.

Of course the sweetest part was when the staff from AIIM returned and they found out what we had done in their absence.

Of course we did this at the next AIIM. And the next one. In fact, we started looking at the sales trade show schedule for when we should run the next bacon day. We invested in a second grill because our headcount was larger. For some reason, Kevin Hulse and I nearly always piloted the grills. Eventually we broke down and grudgingly scheduled bacon day when when everyone was available. You know, just to stop sales from bellyaching.

One thing I particularly liked about the process of preparing for bacon day was that Burger’s online form has a field for putting in a personal message. Christina would fill that out with a message to herself or us. Something along the lines of “Hello, Christina. I am your bacon.” I loved this.

Really, what all this comes down to is that in a small office, you have lots of opportunities to improve your office and the office culture. Generally speaking, you make the rules and a small treat now and again is a very good thing.