Skip to content

Commit

Permalink
clean up README
Browse files Browse the repository at this point in the history
  • Loading branch information
planttheidea committed Dec 16, 2023
1 parent 013acd8 commit e78a4dc
Showing 1 changed file with 84 additions and 85 deletions.
169 changes: 84 additions & 85 deletions README.md
Expand Up @@ -13,8 +13,9 @@ Iteration helpers that inline to native loops for performance
- [Aggressive inlining](#aggressive-inlining)
- [Bailout scenarios](#bailout-scenarios)
- [Gotchas](#gotchas)
- [Conditionals do not delay execution](#conditionals-do-not-delay-execution)
- [`*Object` methods do not perform `hasOwnProperty` check](#object-methods-do-not-perform-hasownproperty-check)
- [`findIndex` vs `findKey`](#findindex-vs-findkey)
- [`find*` methods](#find-methods)
- [Development](#development)

## Summary
Expand All @@ -27,7 +28,7 @@ You can use it for everything, only for hotpaths, as a replacement for `lodash`

## Usage

```javascript
```js
import { map, reduce, someObject } from 'inline-loops.macro';

function contrivedExample(array) {
Expand All @@ -52,10 +53,10 @@ function contrivedExample(array) {
- `filterRight` => same as `filter`, but iterating in reverse
- `filterObject` => same as `filter` but iterating over objects intead of arrays
- `find` ([MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find))
- `findRight` => same as `find`, but iterating in reverse
- `findLast` => same as `find`, but iterating in reverse ([MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast))
- `findObject` => same as `find` but iterating over objects intead of arrays
- `findIndex` ([MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex))
- `findIndexRight` => same as `findIndex`, but iterating in reverse
- `findLastIndex` => same as `findIndex`, but iterating in reverse ([MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex))
- `findKey` => same as `findIndex` but iterating over objects intead of arrays
- `flatMap` ([MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap))
- `flatMapRight` => same as `flatMap`, but iterating in reverse
Expand All @@ -67,7 +68,7 @@ function contrivedExample(array) {
- `mapRight` => same as `map`, but iterating in reverse
- `mapObject` => same as `map` but iterating over objects intead of arrays
- `reduce` ([MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce))
- `reduceRight` => same as `reduce`, but iterating in reverse
- `reduceRight` => same as `reduce`, but iterating in reverse ([MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight))
- `reduceObject` => same as `reduce` but iterating over objects intead of arrays
- `some` ([MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some))
- `someRight` => same as `some`, but iterating in reverse
Expand All @@ -77,19 +78,18 @@ function contrivedExample(array) {
Internally Babel will transform these calls to their respective loop-driven alternatives. Example
```javascript
```js
// this
const foo = map(array, fn);

// becomes this
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
const _length = array.length;
const _results = Array(_length);
for (let _key = 0, _value; _key < _length; ++_key) {
_value = array[_key];
_result.push(fn(_value, _key, array));
_results[_key] = fn(_value, _key, array);
}

const foo = _result;
const foo = _results;
```
If you are passing uncached values as the array or the handler, it will store those values as local variables and execute the same loop based on those variables.
Expand All @@ -98,51 +98,56 @@ If you are passing uncached values as the array or the handler, it will store th
One extra performance boost is that `inline-loops` will try to inline the callback operations when possible. For example:
```javascript
```js
// this
const doubled = map(array, value => value * 2);
const doubled = map(array, (value) => value * 2);

// becomes this
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
const _length = array.length;
const _results = Array(_length);
for (let _key = 0, _value; _key < _length; ++_key) {
_value = array[_key];
_result.push(_value * 2);
_results[_key] = _value * 2;
}

const doubled = _result;
const doubled = _results;
```
Notice that there is no reference to the original function, because it used the return directly. This even works with nested calls!
```javascript
```js
// this
const isAllTuples = every(array, tuple => every(tuple, value => Array.isArray(value) && value.length === 2));
const isAllTuples = every(array, (tuple) =>
every(tuple, (value) => Array.isArray(value) && value.length === 2),
);

// becomes this
let _result = true;

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
_value = array[_key];

let _result2 = true;

for (let _key2 = 0, _length2 = _value.length, _value2; _key2 < _length2; ++_key2) {
_value2 = _value[_key2];

if (!(Array.isArray(_value2) && _value2.length === 2)) {
_result2 = false;
let _determination = true;
for (
let _key = 0, _length = array.length, _tuple, _result;
_key < _length;
++_key
) {
_tuple = array[_key];
let _determination2 = true;
for (
let _key2 = 0, _length2 = _tuple.length, _value, _result2;
_key2 < _length2;
++_key2
) {
_value = _tuple[_key2];
_result2 = Array.isArray(_value) && _value.length === 2;
if (!_result2) {
_determination2 = false;
break;
}
}

if (!_result2) {
_result = false;
_result = _determination2;
if (!_result) {
_determination = false;
break;
}
}

const isAllTuples = _result;
const isAllTuples = _determination;
```
### Bailout scenarios
Expand All @@ -152,14 +157,15 @@ Inevitably not everything can be inlined, so there are known bailout scenarios:
- When using a cached function reference (we can only inline functions that are statically declared in the macro scope)
- When there are multiple `return` statements (as there is no scope to return from, the conversion of the logic would be highly complex)
- When the `return` statement is not top-level (same reason as with multiple `return`s)
- The `this` keyword is used (closure must be maintained to guarantee correct value)
That means if you are cranking every last ounce of performance out of this macro, you want to get cozy with ternaries.
That means if you are cranking every last ounce of performance out of this macro, you may want to get cozy with ternaries.
```js
import { map } from 'inline-loops.macro';

// this will bail out to storing the function and calling it in the loop
const deopted = map(array, value => {
const deopted = map(array, (value) => {
if (value % 2 === 0) {
return 'even';
}
Expand All @@ -168,7 +174,7 @@ const deopted = map(array, value => {
});

// this will inline the operation and avoid function calls
const inlined = map(array, value => (value % 2 === 0 ? 'even' : 'odd'));
const inlined = map(array, (value) => (value % 2 === 0 ? 'even' : 'odd'));
```
## Gotchas
Expand All @@ -180,60 +186,58 @@ Some aspects of implementing this macro that you should be aware of:
If you do something like this with standard JS:
```js
return isFoo ? array.map(v => v * 2) : array;
return isFoo ? array.map((v) => v * 2) : array;
```
The `array` is only mapped over if `isFoo` is true. However, because we are inlining these calls into `for` loops in the scope they operate in, this conditional calling does not apply with this macro.
```js
// this
return isFoo ? map(array, v => v * 2) : array;
return isFoo ? map(array, (v) => v * 2) : array;

// turns into this
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
const _length = array.length;
const _results = Array(_length);
for (let _key = 0, _value; _key < _length; ++_key) {
_value = array[_key];
_result[_key] = _value * 2;
_results[_key] = fn(_value, _key, array);
}

return isFoo ? _result : array;
return isFoo ? _results : array;
```
Notice the mapping occurs whether the condition is met or not. If you want to ensure this conditionality is maintained, you should use an `if` block instead:
```js
// this
if (isFoo) {
return map(array, v => v * 2);
return map(array, (v) => v * 2);
}

return array;

// turns into this
if (isFoo) {
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
const _length = array.length;
const _results = Array(_length);
for (let _key = 0, _value; _key < _length; ++_key) {
_value = array[_key];
_result[_key] = _value * 2;
_results[_key] = fn(_value, _key, array);
}

return _result;
return _results;
}

return array;
```
This will ensure the potentially expensive computation only occurs when necessary.
This will ensure the potentially-expensive computation only occurs when necessary.
### `*Object` methods do not perform `hasOwnProperty` check
The object methods will do operations in `for-in` loop, but will not guard via a `hasOwnProperty` check. For example:
```javascript
```js
// this
const doubled = mapObject(object, value => value * 2);
const doubled = mapObject(object, (value) => value * 2);

// becomes this
let _result = {};
Expand All @@ -250,48 +254,43 @@ const doubled = _result;
This works in a vast majority of cases, as the need for `hasOwnProperty` checks are often an edge case; it only matters when using objects created via a custom constructor, iterating over static properties on functions, or other non-standard operations. `hasOwnProperty` is a slowdown, but can be especially expensive in legacy browsers or non-JIT environments.
If you need to incorporate this, you can do it one of two ways:
**Add filtering (iterates twice, but arguably cleaner semantics)**
```javascript
const raw = mapObject(object, (value, key) => (object.hasOwnProperty(key) ? value * 2 : null));
const doubled = filterObject(raw, value => value !== null);
```
If you need to incorporate this, you can just filter prior to the operation:
**Use reduce instead (iterates only once, but a little harder to grok)**
```javascript
const doubled = reduceObject(object, (_doubled, value, key) => {
if (object.hasOwnProperty(key)) {
_doubled[key] = value * 2;
}

return _doubled;
});
```js
const filtered = filterObject(object, (_, key) => Object.hasOwn(object, key));
const doubled = mapObject(filtered, (value) => value * 2);
```
### `findIndex` vs `findKey`
### `find*` methods
Most of the operations follow the same naming conventions:
- `{method}` (incrementing array)
- `{method}Right` (decrementing array)
- `{method}Object` (object)
The exception to this is `findIndex` / `findIndexRight` (which are specific to arrays) and `findKey` (which is specific to objects). The rationale should be obvious (arrays only have indices, objects only have keys), but because it is the only exception to the rule I wanted to call it out.
The exception to this is the collection of `find`-related methods:
- `find`
- `findLast`
- `findObject`
- `findIndex`
- `findLastIndex`
- `findKey`
The reason for `findLast` / `findLastIndex` instead of `findRight` / `findIndexRight` is because unlike all the other right-direction methods, those are part of the ES spec. Additionally, the reason for `findIndex` vs `findKey` is semantic, as objects have keys and arrays have indices.
## Development
Standard stuff, clone the repo and `npm install` dependencies. The npm scripts available:
- `build` => runs babel to transform the macro for legacy NodeJS support
- `copy:types` => copies `index.d.ts` to `build`
- `dist` => runs `build` and `copy:types`
- `clean`=> remove any files from `dist`
- `lint` => runs ESLint against all files in the `src` folder
- `lint:fix` => runs `lint`, fixing any errors if possible
- `prepublishOnly` => run `lint`, `test`, `test:coverage`, and `dist`
- `release` => release new version (expects globally-installed `release-it`)
- `release:beta` => release new beta version (expects globally-installed `release-it`)
- `prepublishOnly` => run `lint`, `typecheck`, `test`, `clean`, `and `dist`
- `release` => release new version
- `release:beta` => release new beta version
- `test` => run jest tests
- `test:watch` => run `test`, but with persistent watcher
- `typecheck` => run `tsc` against the codebase

0 comments on commit e78a4dc

Please sign in to comment.