Skip to content

Instantly share code, notes, and snippets.

@danielchappell
Last active October 21, 2019 11:51
Show Gist options
  • Save danielchappell/bac3c2e058e7fd7bfb89 to your computer and use it in GitHub Desktop.
Save danielchappell/bac3c2e058e7fd7bfb89 to your computer and use it in GitHub Desktop.
Computed Properties Best Practices

Computed Property Theory & Best Practices or Functional Programming in Ember or How I learned to stop worrying and love the Tomster.

In a nutshell, computed properties let you declare functions as properties. You create one by defining a computed property as a function, which Ember will automatically call when you ask for the property. You can then use it the same way you would any normal, static property. -- The Ember Guides

The Ember Object Model is the corner stone of Ember, and at the heart of the Object Model are computed properties.

The guides do a fine job giving need to know information on how to create and use computed properties and what to expect from the cacheing system they provide. However, I feel so much of the beauty that computed properties provide is lost in terse (but wonderful) documentation.

Quick..the basics!

let Circle = Ember.Object.extend({
    radius: null,
    diameter: Ember.computed('radius', function() {
        return this.get('radius') * 2;
    });
});

let circle = Circle.create({radius: 5});

circle.get('diameter'); //10
circle.set('radius', 10);
circle.get('diameter'); //20

We have created a Circle prototype that declares a radius and provides diameter as a computed property.

We see as we change the value of radius the value of diameter changes accordingly, pretty neat.

Computed properties are defined with the function Ember.Computed(arguments) where the arguments are each Dependent Key in string form, and finally a function that is a pure calculation based on the dependent keys.

Dependent keys should be thought of as the real arguments of the function that are provided for the calculation. This is where all the power lies, more on that in a bit!

What if we only know diameter? Our code from above is not enough to allow us to set a computed property. Right now we can only get the value. Lets add more code.

let Circle = Ember.Object.extend({
    radius: null,
    diameter: Ember.computed('radius', {
        get() {
            return this.get('radius') * 2;
        },
        set(key,value) {
            this.set('radius', value / 2);
            return value;
        }
    })
});

let circle = Circle.create({diameter: 20});

circle.get('radius'); //10
circle.set('radius', 50);
circle.get('diameter'); //100

We have now set a full two way relationship between radius and diameter.

circle_prototype_uml

In the diagram we see that computed properties live on the prototype, where they live happy lives and manage their own values. Without a set function computed properties cannot accept a value and the value instead is set with the object's "own properties". Once a property of the same name lives on the object itself the computed property is effectively over written and can no longer be accessed.

It can sometimes be convenient to have a computed property represent a default that is overwritten. However, this code is not very clear to our future selves or other developers. In general this is an anti-pattern, computed properties should always automatically manage their values and regular properties should be managed manually from the start.

Important Best Practice -- The set function provided should use the given value with the inverse of the get function's calculation to properly set the dependent keys. You should also return the passed value in order to cache it. Any other side effects or mutation in this function is an anti-pattern and can damage the reliability of using your computed properties.

Additional Disclaimer-- Not returning a value from a set function causes the computed property to cache undefined as the value. You should always return value in your computed properties.

Lets finish our circle!

let Circle = Ember.Object.extend({
    radius: null,
    diameter: Ember.computed('radius', {
        get() {
            return this.get('radius') * 2;
        },
        set(key, value) {
            this.set('radius', value / 2);
            return value;
        }
    }),
    circumference: Ember.computed('diameter', {
        get() {
            return this.get('diameter') * Math.PI;
        },
        set(key, value) {
            this.set('diameter', value / Math.PI);
            return value;
        }
    }),
    area: Ember.computed('radius', {
        get() {
            return Math.pow(this.get('radius'), 2) * Math.PI;
        },
        set(key, value) {
            this.set('radius', Math.sqrt(value / Math.PI));
            return value;
        }
    })
});

Here is the data flow.

circle computed property data flow

With our Circle prototype we can provide any 1 property and the rest are calculated for us, but if we provide more that is ok too.

Functional Roots and Theory

In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. -- Wikipedia(2015)

So functional programming is the use of stateless functions passed to each other to create larger calculations with out maintaining state or using mutation.

No mutation; functions that are calculations... sound familiar? Oh yes this is how we have been writing our computed properties!

Ember uses computed properties to facilitate functional programming -- Me(Right now)

Only a few languages have a complete commitment to every function being pure (no mutuation); some examples are Haskell, Idris, and Miranda.

When your functions are pure they are said to be referentially transparent Which means given the same arguments the result or return value of the function will always be the same, no exceptions.

Purely functional implementations come with 3 main benefits:

  1. Lazy evaluation
  2. Easy cacheing
  3. Declarative code.

Lazy evaluation

Pure functional languages evaluate their code only when needed and not when executing code is parsed, this is called "lazy evaluation". The ability to do this is a result of referential transparency. If you are not maintaining and mutating state the timing and order functions are executing does not affect their result.

What does that mean for us in Ember? Well earlier I mentioned dependent keys as the true arguments for their respective computed functions. Much like pure functions in Haskell, Ember computed properties when written idiomatically are referentially transparent.

Ember is able to achieve lazy like evaluation when accessing computed properties. It is not true laziness like in Haskell but Ember is able to postpone the property calculation until it is actually requested, either by template or another property that has it as a dependent key.

Easy cacheing

If our functions have referential transparency then we do not need to execute a function with the same arguments more than once, since we know it will have the same result. This saves time and therefore makes our program faster.

Ember achieves this by observing value changes on a computed property's dependent keys. Once a computed property is asked for and the calculation has been made, the value will be cached until a change is detected in any of it's dependent keys. The cache is cleared when a change is observed but the computed property is not immediately recalculated, instead waiting until it is requested again; because of lazy evaluation.

Important Best Practice Any data that is used in the function that lives external to the function, the dynamic pieces, must be able to reset the cache. Only object properties can be observed so computed properties are limited to other properties as arguments and should avoid using closured variables in their calculations.

Declarative code

The ability to write more declarative and descriptive code is enabled by the lazy evaluation and cacheing mechanisms. These mechanisms work to make our implementation more performant, which allows us to not worry about writing code optimized for a computer and instead optimize code for human readability and have the implementation reflect our model more directly.

Examples

Let check out how these benefits manifest in our circle prototype!

let Circle = Ember.Object.extend({
    radius: null,
    diameter: Ember.computed('radius', {
        get() {
            return this.get('radius') * 2;
        },
        set(key, value) {
            this.set('radius', value / 2);
            return value;
        }
    }),
    circumference: Ember.computed('diameter', {
        get() {
            return this.get('diameter') * Math.PI;
        },
        set(key, value) {
            this.set('diameter', value / Math.PI);
            return value;
        }
    }),
    area: Ember.computed('radius', {
        get() {
            return Math.pow(this.get('radius'), 2) * Math.PI;
        },
        set(key, value) {
            this.set('radius', Math.sqrt(value / Math.PI));
            return value;
        }
    })
});

// radius is directly set no computed properties calculated
let circle = Circle.create({radius: 5});
//At this point diameter is calculated since it is a
//dependent key, then circumference is calculated
circle.get('circumference');
//cached value provided -- no calculation
circle.get('diameter');
// the function sets radius with inverse calculation
//which clears cache for diameter and area is cached with new value
circle.set('area', 34.23423);
//recalculated because radius has changed
circle.get('diameter')
//cached from when we set it before
circle.get('area')

Conclusion

We can't write software, much less a user interface, with out some mutation. What we can do is separate the pure code from the code that contains side effects, and in doing so help us isolate more fragile areas, write better tests, and write cleaner code in general.

Ember has a few other mechanisms for handling code that does not fit the computed property model, namely actions, observers, and hooks. Stay tuned for that article!

Interactive demo that uses this sample code. Pass in any number of circle properties to get the value of the others.

@vikrantsiwachbooker
Copy link

Good one Daniel. Keep it up!

@pusle
Copy link

pusle commented Jun 24, 2018

Thank you for this one. One question; is there something as to many computed properties on a model?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment