Ember-Style Computed Properties in Ruby Gamebox

I’ve been using Ember.js on a recent project because it has a ton features for building web-apps, like routing, event handling, and templated views that use built in data binding. Ember also does a great job of managing data on its objects via its computed properties.

I wanted computed properties in Gamebox, but no Ruby gem existed. So, after reading some Ember.js source, I wrote my own.

Computed Properties in Ember

First, let’s clarify what I mean by computed properties. They’re an easy way to specify a property’s name, any dependencies, and how to calculate the property from its dependencies. Here’s an example right out of the Ember docs:

    App.Person = Ember.Object.extend({
      // these will be supplied by `create`
      firstName: null,
      lastName: null,

      fullName: function() {
        return this.get('firstName') + ' ' + this.get('lastName');
      }.property('firstName', 'lastName')
    });

    var ironMan = App.Person.create({
      firstName: "Tony",
      lastName:  "Stark"
    });

    ironMan.get('fullName'); // "Tony Stark"

Now, whenever a person’s first or last name changes, their full name will update. Also, subsequent calls to fullName will not recompute the value but will return a cached value. You’ll also notice that Ember enforces universal access to the data via get and set. I used both of those ideas in my prototype implementation.

    class Person
      include Props
      prop :first_name
      prop :last_name
       
      prop :full_name do |obj|
      "#{obj.get(:first_name)} #{obj.get(:last_name)}"
      end.depends_on(:first_name, :last_name)
    end
     
    person = Person.create first_name: "Billy", last_name: "Bob"
    p person.get(:full_name)
    # => "Billy Bob"

    person.set(:first_name, "Darth")
    person.set(:last_name, "Vader")
    p person.get(:full_name)
    # => "Darth Vader"

Property Dependencies in Gamebox

So how does this help improve Gamebox? Let take a look at how Gamebox currently manages property dependencies—poorly.

Each property emits a :property_name_changed event that must be subscribed to and managed in a very verbose and imperative way. In this example, there are two properties bounding box and predicted bounding box. The bounding box depends on position, width, height, and rotation. The predicted bounding box depends on the current bounding box, and directional information from velocity and rotational velocity. To ease some of the pain of managing these properties, update_bb just recomputes both bounding boxes always.

    define_behavior :bound_by_box do
      setup do
        actor.has_attributes bb: Rect.new, predicted_bb: Rect.new

        # update_bb updates BOTH the bound box AND the predicted bounding box
        actor.when(:position_changed) { update_bb }
        actor.when(:width_changed) { update_bb }
        actor.when(:height_changed) { update_bb }
        actor.when(:rotation_changed) { update_bb }
        actor.when(:vel_changed) { update_bb }
        actor.when(:rot_vel_changed) { update_bb }
      end
    end

Proposed Prototype

Let’s fix this with our property prototype code. In the reworked example, we are declaring our property, how to update it, and its default value. This description has even made it easier to break down the updating of each individual bounding box at the right time. We’ve taken the imperative pieces out and have a nice succinct description of the data relevant to this behavior in Gamebox. We even get computed property caching for free.

    define_behavior :bound_by_box do
      setup do
        actor.prop :bb do
          calc_bb
        end.depends_on(:position, :width, :height, :rotation).default(Rect.new)

        actor.prop :predicted_bb do
          calc_predicted_bb
        end.depends_on(:bb, :vel, :rot_vel).default(Rect.new)
      end
    end

So far, I’ve only scratched the surface of Ember-style computed properties. There is still a lot to do and explore. I’d like to release this as a gem as soon as I get it integrated into Gamebox. Ember supports nested dependencies like boss.first_name and array properties like my_list.length, my_list.[], or [email protected]. I think this is really a miss in the Ruby community’s gemset. What other features would you like to see from this library?