straits
Version:
Straits is an implementation of traits for JavaScript. It defines some conventions about traits and provides libraries to aid their usage, definition and implementation.
367 lines (269 loc) • 20.6 kB
Markdown
# Straits
Straits is an implementation of traits for JavaScript. It defines some conventions about traits and provides libraries to aid their usage, definition and implementation.
Traits are a way to implement polymorphism: to extend objects and types with extra properties and behavior.
* [Quick example](#quick-example)
* [What are traits?](#what-are-traits)
* [How to use traits?](#how-to-use-traits)
+ [Straits syntax](#straits-syntax)
+ [Traits as member symbols](#traits-as-member-symbols)
+ [Traits as free functions](#traits-as-free-functions)
* [Full example: `clone`](#full-example-clone)
* [Straits vs ...](#straits-vs-)
+ [Classical properties](#classical-properties)
+ [Free functions](#free-functions)
* [FAQ](#faq)
+ [How does straits scoping work?](#how-does-straits-scoping-work)
* [Common errors](#common-errors)
+ [SyntaxError: Unexpected identifier; SyntaxError: Unexpected token *](#syntaxerror-unexpected-identifier-syntaxerror-unexpected-token-)
+ [No trait set is providing symbol x.](#no-trait-set-is-providing-symbol-x)
+ [Symbol x offered by multiple trait sets.](#symbol-x-offered-by-multiple-trait-sets)
* [Status of straits](#status-of-straits)
+ [Symbol versioning](#symbol-versioning)
+ [Trait set extensibility](#trait-set-extensibility)
+ [use trait t from traitSet](#use-trait-t-from-traitset)
* [Who uses straits?](#who-uses-straits)
## Quick example
Imagine that you want to define your own serialization function. It needs to work with primitive types (e.g. `Number`, `String`, `Boolean`, ...) and standard ones (`Object`, `Array`, `Map`...) as well as with custom types defined by you or other libraries. Each type might need to specialize the serialization function in a different way.
What's the correct way to do it?
Our answer is traits!
Here the code, using the [straits syntax](#straits-syntax):
```javascript
// defining the trait set:
//const serializationTraitSet = { serialize: Symbol('serialize') };
// a preferred way to define a trait set::
import {TraitSet} from 'straits';
const serializationTraitSet = new TraitSet('serialize');
// telling the .* operator where to look for traits
use traits * from serializationTraitSet;
// implementing the `serialize` trait for standard types
Object.prototype.*serialize = function() { ... };
Array .prototype.*serialize = function() { ... };
Number.prototype.*serialize = function() { ... };
// implementing the `serialize` trait for custom types
MyCustomType.prototype.*serialize = function() { ... };
...
// using the `serialize` trait on an object
...
object.*serialize();
```
Traits offer many advantages above other possible approaches:
- Traits make it trivial to specialize behavior in a truly polymorphic way.
- Traits never break, disturb or collide with existing code. They're only relevant to the pieces of code where they're explicitly used.
- Traits are fast. They exploit the JavaScript prototype chain without overhead.
- When using the [straits syntax](#straits-syntax), traits aren't bothered by variables in the scope, and avoid polluting the latter.
...Even the JavaScript standard, from ECMAScript 2015, is using them.
## What are traits?
**Traits** are a way to implement polymorphism: to extend objects and types with extra properties and behavior. If you want to learn more about traits, give a read to the [Wikipedia article](https://en.wikipedia.org/wiki/Trait_(computer_programming)).
Traits are implemented using [**`symbol`**](https://developer.mozilla.org/en-US/docs/Glossary/symbol): a JavaScript primitive type, introduced in ECMAScript 2015 for purposes such as this one. It's a special type that can be used as key for object properties. Two different `symbol`s are always different: symbol properties cannot collide. `for(...in...)` loops also ignore `symbol` properties, and if `symbol`s are defined as non-enumerable (like the `.*` operator does), they're completely invisible (the only way to deal with such `symbol`s is by having an instance of it, or by using [`Object.getOwnPropertySymbols()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols)).
Another important building block to master traits (and JavaScript itself) is the [**prototype**-chain](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain). Each object in JavaScript has one prototype (which is a regular object as well). When an object's property is accessed, the property is looked for in the object itself; if it's not found it's searched in the object's prototype; if it's not yet found it's searched in the prototype's prototype, and so on recursively. `symbol`s can be defined on an object and/or on their prototype, just like regular properties.
The ECMAScript 2015 standard defines some standard `symbol`s and uses them as traits. Straits and the [straits syntax](#straits-syntax) is compatible with them. The standard uses names such as "protocols" to refer to them and an example is [`Symbol.iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator) (see [iteration protocols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols)). When using the straits syntax, you can `use traits * from Symbol;` and then use `[].*iterator` and whatnot. Straits aims to introduce traits as a native feature of the language.
## How to use traits?
Straits encourages defining traits in a *trait set*: an object whose keys are regular strings and their values are `symbol`s. This is also what the global object [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) does.
It's possible to use traits using three different styles:
- Using the [straits syntax](#straits-syntax), in the process of getting proposed for the next JavaScript standards. Currently available through a babel plugin: [`straits-babel`](https://github.com/peoro/straits-babel/).
- Using traits as [member symbols](#traits-as-member-symbols).
- Using traits as [free functions](#traits-as-free-functions).
The `straits.utils` module can help defining and using traits and trait sets in all of these fashions.
### Straits syntax
The simplest and most performant way to use traits is by using a new syntax: the `.*` operator and `use traits` statement.
For more details, refer to the [straits-babel](https://github.com/peoro/straits-babel/) documentation: a babel plugin implementing the straits syntax.
Assuming that `myTraitSet` provides a `myTrait` trait, the following piece of code...
```javascript
use traits * from myTraitSet;
object.*myTrait
```
...would be roughly equivalent to:
```javascript
object[ myTraitSet.myTrait ]
```
`.*` can also be used to assign traits, in which case the trait (i.e. `symbol` property) will be not-enumerable.
The `.*` operator only looks for the identifier to its right in the trait sets specified by the `use traits` statements. Variables available in the current scope won't interfere with the `.*` operator.
This syntax makes the code easier to write, read and understand. It avoids collisions between traits and scope variables and makes it easier and painless to import a large number of traits from a library.
The following is a real usage example:
```javascript
import * as scontainers from 'scontainers';
use traits * from scontainers;
const array = [[1],7,[-2,5],2,7,[],4];
const result = array
.*flatten() // [1, 7, -2, 5, 2, 7, 4]
.*reverse() // [4, 7, 2, 5, -2, 7, 1]
.*filter( item=>item%2 !== 0 ) // [7, 5, 7, 1]
.*map( item=>item**2 ) // [49, 25, 49, 1]
.*sum();
// result: 124
```
### Traits as member symbols
It's possible to use traits directly, as they're just `symbol`s. Code written this way will offer the same performance as the `.*` syntax.
Be careful when assigning traits this way: assigning the trait directly (i.e. `obj[trait] = value;`) will result in the trait being enumerable. One should use `Object.defineProperty(obj, trait, {value:value, configurable:true});` instead; that's what happens when `obj.*trait = value;` is evaluated.
The latest example from above could look like this:
```javascript
import * as scontainers from 'scontainers';
const array = [[1],7,[-2,5],2,7,[],4];
const result = array
[scontainers.flatten]() // [1, 7, -2, 5, 2, 7, 4]
[scontainers.reverse]() // [4, 7, 2, 5, -2, 7, 1]
[scontainers.filter]( item=>item%2 !== 0 ) // [7, 5, 7, 1]
[scontainers.map]( item=>item**2 ) // [49, 25, 49, 1]
[scontainers.sum]();
// result: 124
```
### Traits as free functions
It's possible to create free functions that wrap traits.
This could introduce a small overhead, as the free function is an indirection, but it has the advantage of working with `null` and `undefined`.
The `straits.utils` module offers functions to generate free functions from traits. Trait sets defined with `starits-utils` (i.e. with `TraitsSet` as prototype) already offer a `freeFunction` property to obtain a set of free functions from a trait set.
Here is once again the above example written using this approach:
```javascript
import * as scontainers from 'scontainers';
const _ = scontainers.freeFunctions;
// equivalent to:
//import * as straits from 'straits';
//import * as scontainers from 'scontainers';
//use traits * from straits.utils;
//const _ = scontainers.*traitsToFreeFunctions();
const array = [[1],7,[-2,5],2,7,[],4];
// use `_` like you would use underscore or lodash:
const result =
_.sum(
_.map(
_.filter(
_.reverse(
_.flatten(array) // [1, 7, -2, 5, 2, 7, 4]
), // [4, 7, 2, 5, -2, 7, 1]
item=>item%2 !== 0
), // [7, 5, 7, 1]
item=>item**2
) // [49, 25, 49, 1]
);
// result: 124
```
## Full example: `clone`
Imagine that you want to define a `clone` function.
`clone` would need to be polymorphic: it needs to be implemented in different ways for `Object`s, `Array`s, `Number`s etc; some custom types might also need a custom serialization method.
You should define a `clone` trait, implement it for existing types and let other developers know that if needed they can implement such trait for the types they define:
```javascript
//const cloneTraits = {
// clone: Symbol('clone')
//};
// or even better:
import * as straits from 'straits';
const cloneTraits = new straits.utils.TraitSet('clone');
use traits * from cloneTraits;
Object.prototype.*clone = (obj)=>Object.create({}, obj);
Array.prototype.*clone = (arr)=>arr.slice()
Number.prototype.*clone = (num)=>num;
String.prototype.*clone = (num)=>str;
...
// implementing the `clone` behavior for `null` and `undefined`
// it will be used when the clone free function is called on something
cloneTraits.clone.*implDefault( function(subject) {
if( subject === undefined || subject === null ) {
return subject;
}
throw new Error(`${subject.toString()} cannot be cloned.`);
});
export default cloneTraits;
```
Note that a `clone` symbol is already defined in `straits.common`. That symbol should be used when implementing a similar semantics.
## Straits vs ...
Traits are arguably the best way to implement polymorphism, especially when one wishes to implement a new polymorphic behavior on existing types.
Of course there are other ways to implement anything. Let's see some alternatives, taking the [`serialize` example](#quick-example) in consideration.
### Classical properties
The simplest way to implement the `serialize` behavior could be by using a classical (string) property `"serialize"`:
```javascript
MyCustomType.prototype.serialize = function(){ ... };
```
The big downside of this approach is that you should never modify existing types or objects (with non-`symbol` properties). That's very bad practice and it could quickly lead to undefined behaviors.
The problem is that existing code relies on properties (features; attributes; characteristics) of existing objects.
1. What happens if two different libraries need to define their own serialization function? If they both end up choosing the same name (e.g. `serialize`), they would override each other's implementation, At least one of the libraries would call the wrong function and this will surely cause problems.
2. Existing code could iterate on the properties of existing objects, and the unexpected encounter of a new, non-standard property could cause undefined behavior.
3. Imagine that `Object.keys` and `Object.values` were members of `Object.prototype`. How could you use those functions on the object ``{ keys:[1,2,3], values:['a', 'b', 'c'] }``? The member functions would be overridden by the object's own properties.
That's the reason why no decent library does this. [Underscore](http://underscorejs.org/) and [lodash](https://lodash.com/), expose free functions. [jQuery](http://jquery.com/) encapsulates their data in custom objects rather than using `Array` etc.
`symbol` properties can achieve exactly the same result while avoiding problems. They used to be more awkward to use, but the [straits syntax](#straits-syntax) is here to fix this issue.
### Free functions
One could define `serialize` as a free function. The free function could enter different branches of code depending on the type of the object to serialize, but how could somebody specialize the behavior for a custom type introduced by a third library?
This is what libraries like [Underscore](http://underscorejs.org/) and [lodash](https://lodash.com/), do, but the result is that they only support very few types (i.e. `Object` and `Array`). They don't even support `Map` and `Set`; let alone custom types.
It could be possible to think of a way to register the specialization of a function in some data structure, but this could easily degrade performance, introduce memory leaks, fail to work with mixins etc. [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) (introduced in ECMAScript 2015 along with `symbol`) would be very useful for this task, but the result would most likely be inferior to using traits.
## FAQ
### How does straits scoping work?
The `.*` operator looks for its right identifier in a different scope from the regular variable's scope. The scope it uses is populated by the `use traits` statements.
The traits scopes have a visibility similar to regular scopes: they are valid only for the current block and all its nested ones.
Traits used in inner scopes don't override those defined in outer scopes though, as shown in the following example:
```javascript
const traits1 = { x:Symbol() };
use traits * from traits1;
{
const traits2 = { x:Symbol() };
use traits * from traits2;
// Error!
// Symbol x offered by multiple trait sets.
[].*x;
}
```
This is to avoid problems with API changes: imagine in fact that at the beginning of the development only `traits1` defined the `x` trait. Code everywhere was written relying on that specific trait.
If later during the development `traits2` adds an `x` trait, the existing code should not start using that one, as the semantics may differ.
Ideally it should be possible to explicitly say in which trait set to look for a trait:
```javascript
const traits1 = { x:Symbol() };
use traits x, * from traits1; // `x` is only looked for in `traits1`
{
const traits2 = { x:Symbol() };
use traits * from traits2;
[].*x; // same as `[][traits1.x]`
}
```
But this hasn't been implemented yet.
## Common errors
### SyntaxError: Unexpected identifier; SyntaxError: Unexpected token *
The `use traits` statement and `.*` operator aren't standard JavaScript. They're a proposed extension.
Currently, the only way to use them is transpiling them to valid JavaScript using [straits-babel](https://github.com/peoro/straits-babel/).
### No trait set is providing symbol x.
One of the traits accessed with the `.*` operator is not provided by any trait set:
```javascript
const traitSet = {};
use traits * from traitSet;
[].*x;
```
### Symbol x offered by multiple trait sets.
One of the traits accessed with the `.*` operator is provided by 2 or more trait sets:
```javascript
const traitSet1 = { x:Symbol() };
const traitSet2 = { x:Symbol() };
use traits * from traitSet1;
use traits * from traitSet2;
[].*x;
```
## Status of straits
Straits is still in its alpha stage. It already works and it's proving itself very useful, but a few important features are still missing.
### Symbol versioning
Currently, if two different versions of a library that uses straits are used in the same project, all the symbols exposed by such library are duplicated.
Let's consider a concrete example: `scontainers`.
Let's say that the semantics of the `flatten` trait changes. The version of `scontainers` will be bumped, and new projects will start using the new version. Existing code might continue using the old one for a while.
When both versions of `scontainers` are loaded in a project, two different full sets of traits will be created and coexist. The two versions of the same symbols (e.g. `map`, as well as `flatten`) will both be implemented for the standard types. The containers you defined, instead, will only implement the version of the traits your code is using. Some other modules used in the project (the ones that use the other version of `scontainers`) won't be able to use the container traits implemented on your objects.
The current behavior could be ok, but a different behavior might be preferable.
The traits whose semantics didn't change should use the same symbol even among two different versions of `scontainers`. Only the traits whose semantics changed should use different symbols (and thus different implementations).
This requires somehow versioning the symbols. It's something that a library (e.g. `scontainers`) could already do, but we believe that there should be a standard way and that `straits.utils` should offer an API to aid the effort.
### Trait set extensibility
In most programming languages supporting traits (e.g. rust, haskell, go etc), one can automatically implement new traits for all the types that implement some other traits.
This cannot be easily done in JavaScript, as it's a dynamic language and virtually anything can implement traits at any point of the execution. Keeping a database of which objects are implementing which trait is not only very resource consuming, but would also result in memory leaks.
This feature would be very useful in JavaScript as well. Think of `scontainers`: a new trait `whileEach` could easily be implemented for every object that implements `forEach`. But how can we achieve that?
As above, we need to choose a convention to extend traits, and `straits.utils` should offer aiding functions.
### use trait t from traitSet
It might happen that two different trait sets expose traits with the same name.
For instance `straits.math.log` is used to compute the logarithm function (i.e. `Math.log`), while `straits.console.log` is used to print values to the console (i.e. `console.log`).
If your code wants to both `use traits * from straits.math` and `use traits * from straits.console`, `.*log()` can not currently be used.
An advanced version of the `use traits` statement could help us here.
Imagine the following piece of code:
```javascript
import * as straits from 'straits';
use traits * from straits.math;
use traits log, * from straits.console;
7.*log();
```
It means that all the traits are imported both from `straits.math` and from `straits.console`, but we are going to use the `log` symbol from `straits.console`.
[`straits-babel`](https://github.com/peoro/straits-babel/) does not implement this syntax yet.
## Who uses straits?
- [`scontainers`](https://github.com/peoro/scontainers/), a container library that offers functional-style traits for containers. It achieves great performances and a polished semantics.
- [`esast`](https://github.com/peoro/esast/), a library to manipulate JavaScript AST. Used by `scontainers` to generate efficient code on the fly.
- `straits.core`, `straits.console` `straits.math` `straits.reflect`, modules exposing most of the free functions member of `Object`, `console`, `Map`, `Reflect` etc as traits.
- `straits.common`, exposes a bunch of generic symbols that should be implemented by most types.
- `Symbol`, the standard global object `Symbol` is compatible with straits.