Trait

trait~ Trait

Helper for implementing generic functions/protocols.

// Declaring a trait
const Size = new Trait('Size');

// Using it
const size = (what) => Size.invoke(what);
const empty = (what) => size(what) === 0;

// Providing implementations for own types
class MyType {
  [Size.sym]() {
    return 42;
  }
}

// Providing implementations for third party types
Size.impl(Array, (x) => x.length); // Method of type Array
Size.impl(String, (x) => x.length);
Size.impl(Map, (x) => x.size);
Size.impl(Set, (x) => x.size);

Size.impl(Object, (x) => { // Note that this won't apply to subclasses
  let cnt = 0;
  for (const _ in x) cnt++;
  return cnt;
});

// Note: The two following examples would be a bad idea in reality,
// they are just here toshow the mechanism
Size.implStatic(null, (_) => 0); // Static implementation (for a value and not a type)

// This implementation will be used if the underlying type/value
// implements the magnitude trait
Size.implDerived([Magnitued], ([magnitude], v) => magnitude(v));

// This will be called as a last resort, so this must be very fast!
// This example would implement the `size` trait for any even number.
// Note how we just return `undefined` for non even numbers
Size.implWildStatic(
   (x) => type(x) === Number && x % 2 == 0 ? (x => x) : undefined);

// test if an object is a dom node
const isNode = o =>
    typeof Node === "object"
       ? o instanceof Node
       : o && typeof o === "object"
           && typeof o.nodeType === "number"
           && typeof o.nodeName==="string";

// Last resort lookup for types. Implements Size for any dom nodes…
Size.implWild(
   (t) => isNodeType(t) ? ((elm) => elm.childElementCount) : undefined);


// Using all the implementations
size([1,2,3]) # => 3
size({foo: 42}) # => 1
size(new Set([1,2,3])) # => 3
size(new MyType()) # => 42
size(null) # => 0
size(document.body) # => 1

Traits, an introduction: Very specific interfaces that let you choose your guarantees

This helps to implement a concept known as type classes in haskell, traits in rust, protocols in elixir, protocols (like the iteration protocol) in javascript. This helper is not supposed to replace ES6 protocols, instead it is supposed to expand on them and make them more powerfull.

Basically this allows you to declare an interface, similar to interfaces in C++ or C# or Java. You declare the interface; anyone implementing this generic interface (like the iterator protocol, or Size interface which can be used to determine the size of a container) promises to obey the rules and the laws of the interface. This is much more specific than having a size() method for instance; size() is just an name which might be reasonably used in multiple circumstances; e.g. one might use the name size() for a container that can have a null size, or return a tuple of two numbers because the size is two dimensional. Or it might require io to return the size or be complex to compute (e.g. in a linked list).

A size() method may do a lot of things, the Size trait however has a highly specific definition: It returns the size of a container, as a Number which must be greater than zero and cannot be null. The size must be efficient to compute as well.

By using the Size trait, the developer providing an implementation specifically says 'I obey those rules'. There may even be a second trait called Size with it's own rules. The trait class is written in a way so those two would not interfere.

Traits do not provide type checks

Because we are in javascript, these guarantees are generally not enforced by the type system and the dev providing an implementation is still responsible for writing extensive tests.

Traits provide abstraction: Think about what you want to do, not how you want to do it

One specific feature traits provide is that they let you state what you want to do instead of how to do it. Need to determine the size of a container? Use .length for arrays and strings, use .size for ES6 Maps and Sets and a for loop to determine the size of an object. Or you could just use the Size trait and call size(thing) which works for all of these types. This is one of the features traits provide; define an implementation for a trait once and you no longer have to think about how to achieve a thing, just what to achieve.

Implementing traits for third party types

This is another feature that makes traits particularly useful! Java for instance has interfaces, but the creator of a class/type must think of implementing a specific interface; this is particularly problematic if the type is from a library; the interface must either come from the standard library or from that particular library.

This usually is not very helpful; with traits this is not a problem at all. Just use MyTrait.impl as in the example above.

Subclassing the Trait class

You may subclass Trait and overwrite any of it's methods.

Constructor

new Trait(name, sym)

Source:
Properties:
Name Type Description
name String | undefined

The name of the trait

sym Symbol

The sym­bol for lookup in­side third party classes

Parameters:
Name Type Description
name string

The name of the trait

sym Symbol | null

Sym­bol as­so­ci­ated with the trait; this sym­bol will be avail­able un­der MyTrait.sym for devs to im­ple­ment their in­ter­faces with. This para­meter is usu­ally left empty; in this case a new sym­bol is cre­ated for the trait. An ex­ample where the ex­tra para­meter is used is the Se­quence trait in se­quence.js; this trait is just a wrap­per around the built in Sym­bol.iter­ator pro­tocol, so it's us­ing it's sym­bol.

Methods

lookupValue(what) → {function|falsy-value}

Source:

Find the im­ple­ment­a­tion of this trait for a spe­cific value. This is used by .in­voke(), .sup­ports() and .val­ue­Sup­ports.

It uses the fol­low­ing pre­ced­ence by de­fault:

  • Im­ple­ment­a­tions ad­ded with im­pl­Static
  • Im­ple­ment­a­tions us­ing the sym­bol in a method of a pro­to­type
  • Im­ple­ment­a­tions ad­ded with impl
  • Im­ple­ment­a­tions ad­ded with im­plDe­rived in the or­der they where ad­ded
  • Im­ple­ment­a­tions ad­ded with im­pl­Wild in the or­der…
  • Im­ple­ment­a­tions ad­ded with im­pl­Wild­Static in the or­der…

This func­tion can be used dir­ectly in or­der to avoid a double look­iup of the im­ple­ment­a­tion:

const impl = MyTrait.look­up­Value(what);
if (impl) {
  impl(what, ...);
} else {
  ...
}
Parameters:
Name Type Description
what Any

The thing to find an im­ple­ment­a­tion for

Returns:

The func­tion that was found or noth­ing. Takes the same para­met­ers as .in­voke(what, ...args), so if you are not us­ing in­voke, you must spe­cify what twice; once in the look­up­Value call, once in the in­voc­a­tion.

Type
function | falsy-value

lookupType()

Source:

Lookup the im­ple­ment­a­tion of this trait for a spe­cific type. Pretty much the same as look­up­Value, just skips the value lookup steps…

invoke()

Source:

In­voke the im­ple­ment­a­tion. See ex­amples above.

impl()

Source:

Im­ple­ment this trait for a class as a 'meth­od'. See ex­amples above

implStatic()

Source:

Im­ple­ment this trait for a value/​as a 'static meth­od'. See ex­amples above Prefer impl() when pos­sible since im­ple­ment­a­tions us­ing this func­tion will not show up in sup­ports()/​this.type­HasImpl().

implDerived()

Source:

Im­ple­ments a trait based on other traits

implWild()

Source:

Ar­bit­rary code im­ple­ment­a­tion of this trait for types. See ex­amples above Prefer im­pl­Wild() when pos­sible since im­ple­ment­a­tions us­ing this func­tion will not show up in sup­ports()/​this.type­HasImpl().

implWildStatic()

Source:

Ar­bit­rary code im­ple­ment­a­tion of this trait for val­ues. See ex­amples above