This is not an essay about ‘Traits in Javascript’ (updated)

A trait is a concept used in object-oriented programming: a trait represents a collection of methods that can be used to extend the functionality of a class. Essentially a trait is similar to a class made only of concrete methods that is used to e…


This content originally appeared on raganwald.com and was authored by Reginald Braithwaite

Scott Penkava, Untitled (Portrait of Felix in NY), 2009

A trait is a concept used in object-oriented programming: a trait represents a collection of methods that can be used to extend the functionality of a class. Essentially a trait is similar to a class made only of concrete methods that is used to extend another class with a mechanism similar to multiple inheritance, but paying attention to name conflicts, hence with some support from the language for a name-conflict resolution policy to use when merging.—Wikipedia

A trait is like a mixin, however with a trait, we can not just define new behaviour, but also define ways to extend or override existing behaviour. Traits are a first-class feature languages like Scala. Traits are also available as a standard library in languages like Racket. Most interestingly, traits are a feature of the Self programming language, one of the inspirations for JavaScript.

Traits are not a JavaScript feature as this essay is being written, but we can easily make lightweight traits out of the features JavaScript already has.

Our problem is that we want to be able to override or extend functionality from shared behaviour, whether that shared behaviour is defined as a class or as functionality to be mixed in.

our toy problem

Here’s a toy problem we solved elsewhere with a subclass factory that in turn is made out of a an extremely simple mixin.1

To recapitulate from the very beginning, we have a Todo class:

class Todo {
  constructor (name) {
    this.name = name || 'Untitled';
    this.done = false;
  }

  do () {
    this.done = true;
    return this;
  }

  undo () {
    this.done = false;
    return this;
  }

  toHTML () {
    return this.name; // highly insecure
  }
}

And we have the idea of “things that are coloured:”

let toSixteen = (c) => '0123456789ABCDEF'.indexOf(c),
    toTwoFiftyFive = (cc) => toSixteen(cc[0]) * 16 + toSixteen(cc[1]);

class Coloured {
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  }

  luminosity () {
    let {r, g, b} = this.getColourRGB();

    return 0.21 * toTwoFiftyFive(r) +
           0.72 * toTwoFiftyFive(g) +
           0.07 * toTwoFiftyFive(b);
  }

  getColourRGB () {
    return this.colourCode;
  }
}

And we want to create a time-sensitive to-do that has colour according to whether it is overdue, close to its deadline, or has plenty of time left. If we had multiple inheritance, we would write:

let yellow = {r: 'FF', g: 'FF', b: '00'},
    red    = {r: 'FF', g: '00', b: '00'},
    green  = {r: '00', g: 'FF', b: '00'},
    grey   = {r: '80', g: '80', b: '80'};

let oneDayInMilliseconds = 1000 * 60 * 60 * 24;

class TimeSensitiveTodo extends Todo, Coloured {
  constructor (name, deadline) {
    super(name);
    this.deadline = deadline;
  }

  getColourRGB () {
    let slack = this.deadline - Date.now();

    if (this.done) {
      return grey;
    }
    else if (slack <= 0) {
      return red;
    }
    else if (slack <= oneDayInMilliseconds){
      return yellow;
    }
    else return green;
  }

  toHTML () {
    let rgb = this.getColourRGB();

    return `<span style="color: #${rgb.r}${rgb.g}${rgb.b};">${super.toHTML()}</span>`;
  }
}

But we don’t have multiple inheritance. In languages where mixing in functionality is difficult, we can fake a solution by having ColouredTodo inherit from Todo:

class ColouredTodo extends Todo {
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  }

  luminosity () {
    let {r, g, b} = this.getColourRGB();

    return 0.21 * toTwoFiftyFive(r) +
           0.72 * toTwoFiftyFive(g) +
           0.07 * toTwoFiftyFive(b);
  }

  getColourRGB () {
    return this.colourCode;
  }
}

class TimeSensitiveTodo extends ColouredTodo {
  constructor (name, deadline) {
    super(name);
    this.deadline = deadline;
  }

  getColourRGB () {
    let slack = this.deadline - Date.now();

    if (this.done) {
      return grey;
    }
    else if (slack <= 0) {
      return red;
    }
    else if (slack <= oneDayInMilliseconds){
      return yellow;
    }
    else return green;
  }

  toHTML () {
    let rgb = this.getColourRGB();

    return `<span style="color: #${rgb.r}${rgb.g}${rgb.b};">${super.toHTML()}</span>`;
  }
}

The drawback of this approach is that we can no longer make other kinds of things “coloured” without making them also todos. For example, if we had coloured meetings in a time management application, we’d have to write:

class Meeting {
  // ...
}

class ColouredMeeting extends Meeting {
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  }

  luminosity () {
    let {r, g, b} = this.getColourRGB();

    return 0.21 * toTwoFiftyFive(r) +
           0.72 * toTwoFiftyFive(g) +
           0.07 * toTwoFiftyFive(b);
  }

  getColourRGB () {
    return this.colourCode;
  }
}

This forces us to duplicate “coloured” functionality throughout our code base. But thanks to mixins, we can have our cake and eat it to: We can make ColouredAsWellAs a kind of mixin that makes a new subclass and then mixes into the subclass. We call this a “subclass factory:”

function ClassMixin (behaviour) {
  const instanceKeys = Reflect.ownKeys(behaviour);

  return function mixin (clazz) {
    for (let property of instanceKeys)
      Object.defineProperty(clazz.prototype, property, {
        value: behaviour[property],
        writable: true
      });
    return clazz;
  }
}

const SubclassFactory = (behaviour) =>
  (superclazz) => ClassMixin(behaviour)(class extends superclazz {});

const ColouredAsWellAs = SubclassFactory({
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  },

  luminosity () {
    let {r, g, b} = this.getColourRGB();

    return 0.21 * toTwoFiftyFive(r) +
           0.72 * toTwoFiftyFive(g) +
           0.07 * toTwoFiftyFive(b);
  },

  getColourRGB () {
    return this.colourCode;
  }
});

class TimeSensitiveTodo extends ColouredAsWellAs(Todo) {
  constructor (name, deadline) {
    super(name);
    this.deadline = deadline;
  }

  getColourRGB () {
    let slack = this.deadline - Date.now();

    if (this.done) {
      return grey;
    }
    else if (slack <= 0) {
      return red;
    }
    else if (slack <= oneDayInMilliseconds){
      return yellow;
    }
    else return green;
  }

  toHTML () {
    let rgb = this.getColourRGB();

    return `<span style="color: #${rgb.r}${rgb.g}${rgb.b};">${super.toHTML()}</span>`;
  }
}

This allows us to override both our Todo methods and the ColourAsWellAs methods. And elsewhere, we can write:

const ColouredMeeting = ColouredAsWellAs(Meeting);

Or perhaps:

class TimeSensitiveMeeting extends ColouredAsWellAs(Meeting) {
  // ...
}

To summarize, our problem is that we want to be able to override or extend functionality from shared behaviour, whether that shared behaviour is defined as a class or as functionality to be mixed in. Subclass factories are one way to solve that problem.

Now we’ll solve the same problem with traits.

defining lightweight traits

Let’s start with our ClassMixin. We’ll modify it slightly to insist that it never attempt to define a method that already exists, and we’ll use that to create Coloured, a function that defines two methods:

function Define (behaviour) {
  const instanceKeys = Reflect.ownKeys(behaviour);

  return function define (clazz) {
    for (let property of instanceKeys)
      if (!clazz.prototype[property]) {
        Object.defineProperty(clazz.prototype, property, {
          value: behaviour[property],
          writable: true
        });
      }
      else throw `illegal attempt to override ${property}, which already exists.`
  }
}

const Coloured = Define({
  setColourRGB ({r, g, b}) {
    this.colourCode = {r, g, b};
    return this;
  },

  luminosity () {
    let {r, g, b} = this.getColourRGB();

    return 0.21 * toTwoFiftyFive(r) +
           0.72 * toTwoFiftyFive(g) +
           0.07 * toTwoFiftyFive(b);
  },

  getColourRGB () {
    return this.colourCode;
  }
});

Coloured is now a function that modifies a class, adding two methods provided that they don’t already exist in the class.

But we need a variation that “overrides” getColourRGB. We can write a variation of Define that always overrides the target’s methods, and passes in the original method as the first parameter. This is similar to “around” method advice:

function Override (behaviour) {
  const instanceKeys = Reflect.ownKeys(behaviour);

  return function overrides (clazz) {
    for (let property of instanceKeys)
      if (!!clazz.prototype[property]) {
        let overriddenMethodFunction = clazz.prototype[property];

        Object.defineProperty(clazz.prototype, property, {
          value: function (...args) {
            return behaviour[property].call(this, overriddenMethodFunction.bind(this), ...args);
          },
          writable: true
        });
      }
      else throw `attempt to override non-existant method ${property}`;
    return clazz;
  }
}

const DeadlineSensitive = Override({
  getColourRGB () {
    let slack = this.deadline - Date.now();

    if (this.done) {
      return grey;
    }
    else if (slack <= 0) {
      return red;
    }
    else if (slack <= oneDayInMilliseconds){
      return yellow;
    }
    else return green;
  },

  toHTML (original) {
    let rgb = this.getColourRGB();

    return `<span style="color: #${rgb.r}${rgb.g}${rgb.b};">${original()}</span>`;
  }
});

Define and Override are protocols: They define whether methods may conflict, and if they do, how that conflict is resolved. Define prohibits conflicts, forcing us to pick another protocol. Override permits us to write a method that overrides an existing method and (optionally) call the original.

composing protocols

We could now write:

const TimeSensitiveTodo = DeadlineSensitive(
  Coloured(
    class TimeSensitiveTodo extends Todo {
      constructor (name, deadline) {
        super(name);
        this.deadline = deadline;
      }
    }
  )
);

Or:

@DeadlineSensitive
@Coloured
class TimeSensitiveTodo extends Todo {
  constructor (name, deadline) {
    super(name);
    this.deadline = deadline;
  }
}

But if we want to use DeadlineSensitive and Coloured together more than once, we can make a lightweight trait with simple function composition:

const pipeline =
  (...fns) =>
    (value) =>
      fns.reduce((acc, fn) => fn(acc), value);

const SensitizeTodos = pipeline(Coloured, DeadlineSensitive);

@SensitizeTodos
class TimeSensitiveTodo extends Todo {
  constructor (name, deadline) {
    super(name);
    this.deadline = deadline;
  }
}

Now SensitizeTodos combines defining methods with overriding existing methods: We’ve built a lightweight trait by composing protocols.

And that’s all a trait is: The composition of protocols. And we don’t need a bunch of new keywords or decorators (like @overrides) to do it, we just use the functional composition that is so easy and natural in JavaScript.

other protocols

We can incorporate other protocols. Two of the most common are prepending behaviour to an existing method, or appending behaviour to an existing method:

function Prepends (behaviour) {
  const instanceKeys = Reflect.ownKeys(behaviour);

  return function prepend (clazz) {
    for (let property of instanceKeys)
      if (!!clazz.prototype[property]) {
        let overriddenMethodFunction = clazz.prototype[property];

        Object.defineProperty(clazz.prototype, property, {
          value: function (...args) {
            const prependValue = behaviour[property].apply(this, args);

            if (prependValue === undefined || !!prependValue) {
              return overriddenMethodFunction.apply(this, args);;
            }
          },
          writable: true
        });
      }
      else throw `attempt to override non-existant method ${property}`;
    return clazz;
  }
}

function Append (behaviour) {
  const instanceKeys = Reflect.ownKeys(behaviour);

  function append (clazz) {
    for (let property of instanceKeys)
      if (!!clazz.prototype[property]) {
        let overriddenMethodFunction = clazz.prototype[property];

        Object.defineProperty(clazz.prototype, property, {
          value: function (...args) {
            const returnValue = overriddenMethodFunction.apply(this, args);

            behaviour[property].apply(this, args);
            return returnValue;
          },
          writable: true
        });
      }
      else throw `attempt to override non-existant method ${property}`;
    return clazz;
  }
}

We can compose a lightweight trait using any combination of Define, Override, Prepend, and Append, and the composition is handled by pipeline, a plain old function composition tool.

Lightweight traits are nothing more than protocols, composed in a simple and easy-to-understand way. And then applied to simple classes, in a direct and obvious manner.

what do lightweight traits tell us?

Once again we have seen the strength of JavaScript: We don’t need a lot of special language features baked in, provided we are careful to make our existing features out of functions and simple objects. We can then compose them at will using simple tools to make the language features we need.

Over time, when features become popular, those features will get added to the language. But like so many other things either added to ES6 or proposed for future versions, features begin with people rolling their own tools. JavaScript makes this exceptionally easy.

We just have to start with simple things, and combine them in simple ways.

Simplicity is the peak of civilization

the heavyweight. and the light.

When employing a new approach, like traits, there are two ways to do it. The heavyweight way, and the lightweight way.

The lightweight way, as shown here, attempts to be as “JavaScript-y” as possible. For example, using functions for protocols and composing them. With the lightweight way, everything is still just a function, or just an object, or just a class with just a prototype. Lightweight code interoperates 100% with code from other libraries. Lightweight approaches can be incrementally added to an existing code base, refactoring a bit here and a bit there.

The heavyweight way would greenspun a special class hierarchy with support for traits baked in. The heavyweight way would produce “classes” that don’t easily interoperate with other libraries or code, so you can’t incrementally make changes: You have to “boil the ocean” and commit 100% to the new approach. Heavyweight approaches often demand new kinds of tooling in the build pipeline.

When we do things the lightweight way, we make very small bets on their benefits. It’s easy to change our mind and abandon the approach in favour of something else. because we make small bets along the way, we collect on the small benefits continuously: We don’t have to kick off a massive rewrite of our code base to start using lightweight traits, for example. We just start using them as little or as much as we like, and immediately start benefitting from them.

“A language that doesn’t affect the way you think about programming isn’t worth learning.”—Alan J. Perlis

Every tool affects the way we think about programming. But heavyweight tools force us to think about the heavyweight tooling. That thinking isn’t always portable to another tool or another code base.

Whereas lightweight tools are simple things, composed together in simple ways. If we move to a different code base or tool, we can take our experience with the simple things along. With lightweight traits, for example, we are not teaching ourselves how to “program with traits,” we’re teaching ourselves how to “decompose behaviour,” how to “compose functions” and how to “write functions that decorate entities.”

These are all fundamental ideas that apply everywhere, even if we don’t end up applying them to build traits every time we write code. Lightweight thinking is portable and future-proof.

This essay is not, in the end, about how to write traits in JavaScript. Traits are just an example of how the lightweight approach is particularly easy in JavaScript, and an explanation of why that matters.

fin.


postscript from the author: “happy new year!”

As I write this on New Year’s Eve, 2015, I am struck by how much this essay is the same as almost every other essay I’ve written about JavaScript. That’s partly because my brain is shaped by “lightweight” thinking, and partly because JavaScript, for all of its faults, and despite attempts to write heavyweight frameworks for it, is a lightweight language.

People often say that JavaScript wants to be a functional programming language. I believe this is not the whole story: I believe JavaScript wants to be a lightweight programming language, and functions-as-first-class-entities is a deeply lightweight idea. The same is true of newer ideas like classes-as-expressions and decorators-as-functions.

But I repeat myself. Again.

Writing in this modern world is a conversation. With a language. With readers. With fellow language enthusiasts who also write. But conversations run their course. When you find yourself repeating repeating repeating yourself… Perhaps you have made your contribution and it’s time to sidle out and order another whiskey.

I will never say “never again,” but if you do not hear from me on the subject of JavaScript in the future, it is not because I have nothing to say, but rather because I think I have already tried to say it.

Thank you, and I am excited to see what you have to say, in words or in code, in 2016.

—Reginald Braithwaite, Toronto, 2015-12-31


more reading:

notes:

  1. The implementations given here are extremely simple in order to illustrate a larger principle of how the pieces fit together. A production library based on these principles would handle needs we’ve seen elsewhere, like defining “class” or “static” properties, making instanceof work, and appeasing the V8 compiler’s optimizations. 


This content originally appeared on raganwald.com and was authored by Reginald Braithwaite


Print Share Comment Cite Upload Translate Updates
APA

Reginald Braithwaite | Sciencx (2015-12-31T00:00:00+00:00) This is not an essay about ‘Traits in Javascript’ (updated). Retrieved from https://www.scien.cx/2015/12/31/this-is-not-an-essay-about-traits-in-javascript-updated/

MLA
" » This is not an essay about ‘Traits in Javascript’ (updated)." Reginald Braithwaite | Sciencx - Thursday December 31, 2015, https://www.scien.cx/2015/12/31/this-is-not-an-essay-about-traits-in-javascript-updated/
HARVARD
Reginald Braithwaite | Sciencx Thursday December 31, 2015 » This is not an essay about ‘Traits in Javascript’ (updated)., viewed ,<https://www.scien.cx/2015/12/31/this-is-not-an-essay-about-traits-in-javascript-updated/>
VANCOUVER
Reginald Braithwaite | Sciencx - » This is not an essay about ‘Traits in Javascript’ (updated). [Internet]. [Accessed ]. Available from: https://www.scien.cx/2015/12/31/this-is-not-an-essay-about-traits-in-javascript-updated/
CHICAGO
" » This is not an essay about ‘Traits in Javascript’ (updated)." Reginald Braithwaite | Sciencx - Accessed . https://www.scien.cx/2015/12/31/this-is-not-an-essay-about-traits-in-javascript-updated/
IEEE
" » This is not an essay about ‘Traits in Javascript’ (updated)." Reginald Braithwaite | Sciencx [Online]. Available: https://www.scien.cx/2015/12/31/this-is-not-an-essay-about-traits-in-javascript-updated/. [Accessed: ]
rf:citation
» This is not an essay about ‘Traits in Javascript’ (updated) | Reginald Braithwaite | Sciencx | https://www.scien.cx/2015/12/31/this-is-not-an-essay-about-traits-in-javascript-updated/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.