Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use traits in an ES6 class? #1

Open
hackel opened this issue Dec 22, 2015 · 3 comments
Open

Use traits in an ES6 class? #1

hackel opened this issue Dec 22, 2015 · 3 comments
Assignees
Milestone

Comments

@hackel
Copy link

hackel commented Dec 22, 2015

I'm wondering how traits.js should be used in a class, in order to achieve the equivalent of:
class Range(from, to) extends Object uses Enumerable
In the example from the documentation, I am guessing something like this would work:

class EnumerableTrait
{
    constructor()
    {
        return Trait(EnumerableTrait);
    }

    each()
    {
        return Trait.required(); // should be provided by the composite
    }

    map(fun)
    {
        var r = [];
        this.each(function (e) {
            r.push(fun(e));
        });
        return r;
    }

    inject(init, accum)
    {
        var r = init;
        this.each(function (e) {
            r = accum(r, e);
        });
        return r;
    }
}

class Range
{
    constructor(from, to)
    {
        return Trait.create(
            Range,
            Trait.compose(
                EnumerableTrait,
                Trait({
                    each: function(fun) { for (var i = from; i < to; i++) { fun(i); } }
                }
            ))
        );
    }
}

Please let me know if I'm on the right track with this. Would love to use traits in my ES6 projects.

@dotnetCarpenter
Copy link
Member

Hi @hackel.

I'm myself new to traits.js but has helped with moving the project to github. I'll try to give you a comprehensive answer but @tvcutsem is the real expert here.

First of all, traits.js is meant to be used with object literals. An example covering your use-case would in es6 look like this:

"use strict";

const Trait = require("traits.js"); // using CommonJS as we don't support es6 modules yet

const EnumerableTrait = Trait({
  each: Trait.required,
  map(fun) {
    let r = [];
    this.each(e => { r.push(fun(e)); });
    return r;
  },
  inject(init, accum) {
    let r = init;
    this.each(e => { r = accum(r, e)});
    return r;
  }
});

const Range = function(from, to) {
  return Trait.create(
    Object.prototype, // the prototype is insignificant in trait resolution
    Trait.compose(
      EnumerableTrait,
      Trait({
        each(fun) {
          for (var i = from; i < to; i++) {
            fun(i);
          }
        }
      })
    )
  );
}

console.log(
  new Range(0,5).inject(0, (a,b) => a+b) // new is optional
);

Now, if you have a large code base already using classes, then you need to alter their constructors a bit.
You probably already know that instantiating a class returns an object and that class methods are defined on the object's prototype. Since we're using an es6 class which, as far as I know does not have a way to reference other functions, we have to mixin Trait.required in the constructor, so that the returned object contains each: Trait.required.

class EnumerableTrait
{
    constructor() { ... }

    // WE CAN NOT REFERENCE AN EXTERNAL FUNCTION IN A ES6 CLASS
    each(fun) = Trait.required; // Syntax error

We have to change the default object returned from the constructor.

class EnumerableTrait
{
    constructor() {
        return Trait.compose(Trait(EnumerableTrait.prototype), Trait({
          each: Trait.required // should be provided by the composite
        }));
    }

    map(fun) { /* not used */; }

    inject(init, accum) {
        let r = init;
        this.each(e => { r = accum(r, e)});
        return r;
    }
}

Did we go the es5 route, then we would just set the required method name in the prototype.

function EnumerableTrait() {
  return Trait(EnumerableTrait.prototype);
}
EnumerableTrait.prototype = {
  constructor: EnumerableTrait,
  map: function(fun) { /* not used */; },
  inject: function(init, accum) {
    var r = init;
    this.each(function(e) {
      r = accum(r, e);
    });
    return r;
  },
  each: Trait.required // <- EACH IS REQUIRED
}

The two examples are identical, in that they will both produce the following object when instantiated with new EnumerableTrait().

{ constructor:                                                   
   { value: [Function: EnumerableTrait],                         
     writable: true,                                             
     enumerable: false,                                          
     configurable: true,                                         
     method: true },                                             
  map:                                                           
   { value: [Function: map],                                     
     writable: false,                                            
     enumerable: false,                                          
     configurable: false,                                        
     method: true },                                             
  inject:                                                        
   { value: [Function: inject],                                  
     writable: false,                                            
     enumerable: false,                                          
     configurable: false,                                        
     method: true },                                             
  each: { value: undefined, enumerable: false, required: true }
}

The last bit is the Range class which should return a composition of EnumerableTrait and an anonymous trait object with an each implementation.
In es6 style:

class Range
{
    constructor(from, to)
    {
        return Trait.create(
            Object.prototype, // the prototype is insignificant in trait resolution
            Trait.compose(
              new EnumerableTrait, // () is optional when there is no arguments
              Trait({
                each(fun) {
                  for (var i = from; i < to; i++) {
                    fun(i);
                  }
                }
              })
            )
        );
    }
}
console.log(
  new Range(0,5).inject(0, (a,b) => a+b) // new is NOT optional
);

While it is possible to use traits.js with existing classes (which are syntactical sugar for function prototype), you don't achieve any benefits from doing so. traits.js is, IMHO, superior to the native js OOP style. That said, I think that traits.js can and should be made easier to work with es6 classes in the future.

@dotnetCarpenter dotnetCarpenter self-assigned this Jan 8, 2016
dotnetCarpenter added a commit to dotnetCarpenter/eloquent that referenced this issue Jan 8, 2016
@hackel
Copy link
Author

hackel commented Jan 11, 2016

@dotnetCarpenter Thanks for your very thorough response! I will play with this and see what I can do. I'm not about to say which method is superior, but the syntactic sugar of es6 classes definitely makes it easier for those coming from other languages (php, java, etc.), and I think it looks a bit more elegant than using object literals, but that's just me.

@dotnetCarpenter
Copy link
Member

@hackel, well (again) here is the es6 object literal version for comparison:

const EnumerableTrait = Trait({
  each: Trait.required,
  map(fun) {
    let r = [];
    this.each(e => { r.push(fun(e)); });
    return r;
  },
  inject(init, accum) {
    let r = init;
    this.each(e => { r = accum(r, e)});
    return r;
  }
});

const Range = function(from, to) {
  return Trait.create(
    Object.prototype,
    Trait.compose(
      EnumerableTrait,
      Trait({
        each(fun) {
          for (var i = from; i < to; i++) {
            fun(i);
          }
        }
      })
    )
  );
}

console.log(
  new Range(0,5).inject(0, (a,b) => a+b) // new is optional
);

If you're new to JS, you should know that each, map and inject is already part of the language, since es5 for Array and for objects as of es6, as forEach, map and reduce (AKA inject). The example demonstrate an iterator protocol that works in all es versions. traits.js support es3 (IE8 and below).

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

No branches or pull requests

2 participants