stick-js
Version:
Fast toolkit for functional programming in JS. Provides idioms for referentially transparent expressions, clear separation of mutable and immutable operations, object factories, function calls based on English grammar, and pipe & compose operators.
1,871 lines (1,394 loc) • 68.2 kB
Markdown
# Intro
Imagine being able to confidently refactor your work and commit it without
even checking, strictly separate side-effects from pure functions, enforce
immutable operations, mix and match purely referentially transparent
expressions and build functors and monads from simple building blocks.
Now put down that Haskell propaganda manual and listen.
# Quick-start
## node 14.0+
```bash
echo '{}' > package.json
yarn add stick-js @babel/{cli,core} babel-plugin-operator-overload
# or npm i
mkdir src
touch babel.config.js src/index.mjs
```
#### babel.config.js
```javascript
module.exports = (api) => {
api.cache.forever ()
return {
plugins: [
'babel-plugin-operator-overload',
],
}
}
```
#### src/index.mjs
```javascript
defineBinaryOperator ('|', (...args) => pipe (...args))
defineBinaryOperator ('<<', (...args) => compose (...args))
defineBinaryOperator ('>>', (...args) => composeRight (...args))
import {
pipe, compose, composeRight, map, add, join, concat, concatTo,
} from 'stick-js'
const { log, } = console
const { fromCharCode, } = String
const surroundWith = (x) => concatTo (x) >> concat (x)
; [115, 116, 105, 99, 107, 45, 106, 115]
| map (fromCharCode)
| join ('')
| surroundWith ('**')
| log
```
```bash
node --version // v14.5.0
node_modules/.bin/babel --keep-file-extension -d lib src
node lib/index.mjs // **stick-js**
```
It does work with earlier versions of Node, but you will probably need to
use the `.js` suffix instead of `.mjs` and/or use `node -r esm ...` and
`webpack -r esm` and/or use CommonJS imports (see below). Sorry but the
situation is too messy to provide a comprehensive how-to.
## webpack
Here is a working webpack configuration.
### babel.config.js
```javascript
module.exports = {
presets: [
[
'@babel/preset-env',
{
modules: false,
// --- remember to import core-js and regenerator runtime at the very beginning of app.js
useBuiltIns: 'entry',
// --- yarn add core-js@3
corejs: 3,
},
],
],
plugins: [
'operator-overload',
],
}
```
### webpack config:
```javascript
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
...
],
}
```
You can use the `import` form in your frontend source files. You will
probably need to run webpack using `-r esm` in the command line.
## Modules / CommonJS
We provide both CommonJS and ES style modules. If you use `import`, you will
get ES, and if you use `require`, you will get CommonJS. Note that if you
use ES you must be prepared to deal with the ES modules on your side, for
example by using `-r esm` or a recent version of Node.
# Description
- We provide overloaded operators `|`, `>>`, and `<<` and many patterns to
use them with, while [TC39/proposal-pipeline-operator](https://github.com/tc39/proposal-pipeline-operator)
is still being fleshed out.
@TC39, if you're listening: the pipeline operator is extremely limited if
you don't also provide composition operators. We use `>>` (right
composition, the same order as `|`), and `<<` for Haskell-style (left)
composition.
The composition operators can help you abstract away the data from a
pipeline and utilise a 'point-free form'. See below for many examples of
this.
- Using the overloaded operators is opt-in and you do *not* need to use them
if you don't want to. With a few exceptions, all the functions we provide
are useful using a simple data-last calling style as well.
- Note that we use `|`, not `|>`, which is not only easier to overload but
far more pleasant to work with, and which will still work even if/when the
proposal is accepted.
- We provide a toolkit of functions centered around this pattern, and a
number of idioms which they facilitate. In particular, we provide several
ways to replace blocks of statements and control structures with
referentially transparent expressions, and work with prototypes,
factories, mutable and immutable data, and more.
You can use our functions, or your own favorite library (Ramda, lodash/fp,
etc.), or mix and match as you like.
- We make it nice to work with manually curried functions (see below).
- We provide a *pure JavaScript* (or almost-pure, if you overload the
operators) way of using many idioms from Haskell and Racket.
- Many functions whose arguments and argument order are predictible based on
their names and the rules of English grammar.
The overloading is made possible thanks to the great
[babel-plugin-operator-overload](https://github.com/jussi-kalliokoski/babel-plugin-operator-overload)
library by Jussi Kalliokoski (@jussi-kalliokoski).
# TL; DR
Here are a few examples. Source files must begin with a particular header
(omitted here for brevity) -- see 'Overview' section below.
----------
```javascript
const isInteger = x => x === Math.floor (x)
const ifInteger = isInteger | ifPredicate
; [3.5, 4, 4.2]
| map (ifInteger (
x => x + 1,
always ('nothing'),
))
// ['nothing', 5, 'nothing']
```
----------
```javascript
import {
map, join, concat, concatTo,
modulo, gt, eq,
againstBoth, ifPredicate,
} from 'stick-js'
const { log, } = console
const esc = concatTo ('\u001b[')
const color = (code) =>
String >> concatTo (code | concat ('m') | esc) >> concat ('0m' | esc)
const green = '32' | color
const yellow = '33' | color
const odd = modulo (2) >> eq (1)
const oddAndGt4 = againstBoth (odd, gt (4))
const ifOddAndGt4 = oddAndGt4 | ifPredicate
; [3, 4, 5]
| map (ifOddAndGt4 (green, yellow))
| join (' / ')
| log
```

----------
```javascript
const convertFahrenheit = fah => lets (
_ => (fah - 32) / 9 * 5, // (1) celsius
(cel) => cel + 273, // (2) kelvin
(cel, kel) => [cel, kel], // (3) result
)
convertFahrenheit (86) // [30, 303]
```
----------
```javascript
const checkVal = condS ([
4 | eq | guard (sprintf1 ('%s was exactly 4')),
4 | lt | guard (sprintf1 ('%s was less than 4')),
4 | gt | guard (sprintf1 ('%s was more than 4')),
otherwise | guardV ("error, this shouldn't happen"),
])
; [3, 4, 5]
| map (checkVal)
| join (' | ')
// 3 was less than 4 | 4 was exactly 4 | 5 was more than 4
```
----------
```javascript
// ------ dog.js
const proto = { speak () { 'Hi from ' + this.name }}
export default proto | factory
// ------ main.js
import Dog from './dog'
Dog.create ({ name: 'Caesar', }).speak () // 'Hi from Caesar'
```
----------
And much more.
----------
# Examples
- A [WebGL example](http://rain-stick.mister-fish.net), to demonstrate that
stick is fast enough to use for WebGL / animations with many objects
created by factories.
([source](https://github.com/misterfish/rain-webgl-stick), based on a
rewrite of the
amazing [raindrops demo](https://tympanus.net/Development/RainEffect/) by
Lucas Bebber (@lebber).
----------
# Overview
## ٭ basic example ٭
```javascript
// --- source files must begin with this header.
defineBinaryOperator ('|', (...args) => pipe (...args))
defineBinaryOperator ('<<', (...args) => compose (...args))
defineBinaryOperator ('>>', (...args) => composeRight (...args))
import {
pipe, compose, composeRight,
// --- /header
map, join, sprintf1,
} from 'stick-js'
import { green, } from 'chalk'
const { log, } = console
; [1, 2, 3]
| map (x => x + 1)
| join ('/')
| green
| sprintf1 ('The answer is %s')
| log // outputs 'The answer is 2/3/4' (colorfully)
```
## ٭ the 'stick' operator ٭
`a | b` is simply an equivalent way of writing `b (a)`
(What if I really want to do bitwise math, you ask? Don't worry, you still
can: see below).
```javascript
// --- reminder: source files must begin with this.
// --- from here on out we'll omit it in the examples.
defineBinaryOperator ('|', (...args) => pipe (...args))
defineBinaryOperator ('<<', (...args) => compose (...args))
defineBinaryOperator ('>>', (...args) => composeRight (...args))
import {
pipe, compose, composeRight,
map, join, split,
} from 'stick-js'
const multiply = x => y => x * y
const double = multiply (2) // or 2 | multiply
3 | double // 6
double (3) // 6
3 | multiply (4) // 12
const capitaliseFirstLetter = x => x[0].toUpperCase () + x.slice (1)
; `just a perfect day`
| split (' ') // split (' ') is a function
| map (capitaliseFirstLetter) // map (capitaliseFirstLetter) is also a function
| join (' ') // ... you get the picture.
// 'Just A Perfect Day'
```
## ٭ currying styles ٭
All curried functions provided by stick-js can be called using either of 2 currying styles.
This would be a good time to read up on curried functions if you're not familiar with them.
1. we will refer to this sort of function and calling style as 'manual':
const f = a => b => c => a + b + c // call like f (1) (2) (3)
2. and this sort as 'normal':
const g = R.curry ((a, b, c) => a + b + c) // call like g (1) (2) (3)
// or g (1, 2, 3)
// or g (1, 2) (3)
// or g (1) (2, 3)
// etc.
Calling:
```javascript
import { map, } from 'stick-js'
const double = x => x * 2
map (double, [1, 2, 3]) // [2, 4, 6] (normal style)
map (double) ([1, 2, 3]) // [2, 4, 6] (manual style)
; [1, 2, 3] | map (double) // [2, 4, 6] (manual style with stick)
```
For extra performance you can also limit yourself to the manual style (see
below).
## ٭ a note on style ٭
We recommend using a space before the parentheses of a function call.
Admittedly it looks pretty strange at first, but we find that it makes
everything much clearer when you get used to it, in particular with the
manual currying style.
You might also want to check out a vertical alignment plugin. The author
uses `vim-easy-align` by @junegunn.
## ٭ markers ٭
```javascript
import { sprintfN, sprintf1, } from 'stick-js'
3 | sprintf1 ('4 - 1 is %s') // '4 - 1 is 3'
```
'N' is a marker meaning an array is expected.
```javascript
; [4, 3]
| sprintfN ('%s - 1 is %s') // same.
```
'V' means a value is expected, to disambiguate cases where a function also
fits.
```javascript
import { timesV, timesF, } from 'stick-js'
const { random, } = Math
3 | timesV (4) // [3, 3, 3, 3] (`V` stands for 'value')
random | timesF (4) // [<random-num>, <random-num>, <random-num>, <random-num>] (`F` stands for 'function')
random | timesV (4) // [random, random, random, random]
```
Note that the last one stores the function 4 times in the array.
```javascript
random | timesV (4) | map (invoke) // [<random-num>, <random-num>, <random-num>, <random-num>]
```
```javascript
import { appendTo, appendToM, } from 'stick-js'
const a = [1, 2, 3]
const b = 4 | appendTo (a) // functional style: the array is cloned.
b === a // false
const a = [1, 2, 3]
const b = 4 | appendToM (a) // non-functional style: the array is mutated (`M` stands for 'mutable')
b === a // true
const webGLContext = { ... a complicated object ... }
webGLContext | mergeM ({ someProp: false, }) // you probably want mutable here.
```
And there are a few more which we'll see along the way.
## ٭ ok, anaphoric if ٭
`ok (x)` is false if `x` is `null` or `undefined`. Every other input returns
`true`.
```javascript
import { map, ok, notOk, ifOk, } from 'stick-js'
const { log, } = console
; [0, false, '', null, void 8]
| map (ok) // [true, true, true, false, false]
; [0, false, '', null, void 8]
| map (notOk) // [false, false, false, true, true]
```
Something we see a lot in JS is:
```javascript
let answer
if (someVar !== undefined && someVar !== null) {
answer = someVar + 1
} else {
answer = 'nothing'
}
```
This can vastly improved using an 'anaphoric if' and a stick idiom:
```javascript
import { add, always, } from 'stick-js'
const add1 = 1 | add // or add (1)
const answer = someVar | ifOk (add1, 'nothing' | always)
```
Explanation: `ifOk` takes two functions -- a 'then' function and an 'else' function.
In the 'ok' case, the value being tested is passed to the function. (This
bound value is sometimes called an 'anaphor').
```javascript
const add1IfYouCan = val => val | ifOk (
// `that` refers to `val`
that => that + 1,
// no value is passed.
_ => 'nothing',
)
```
This can be further condensed, using 'point-free style' (see below) and the `always` function:
```javascript
const add1IfYouCan = ifOk (1 | add, 'nothing' | always)
```
Usage:
```javascript
const answer = someVar | add1IfYouCan
; [0, 10, null, void 8]
| map (add1IfYouCan) // [1, 11, 'nothing', 'nothing']
```
## ٭ point-free ٭
A common pattern is when the argument to a function is passed immediately into a pipe:
```javascript
const add1IfYouCan = x => x | ifOk (add1, 'nothing' | always)
```
Since `x` does not appear anywhere else in the expression, we can simply remove it, along with the function argument:
```javascript
const add1IfYouCan = ifOk (add1, 'nothing' | always)
```
This is often called 'point-free' style. It means the function arguments and
the data being passed through the pipeline have been abstracted away (and
has nothing to do with dots, despite the name).
If the pipe chain consists of more than 1 link …
```javascript
const { dot, sprintf1, tap, }
const { log, } = console
const add1IfYouCan = x => x
| ifOk (add1, 'nothing' | always)
| String // conversion using type constructor
| dot ('toUpperCase')
| sprintf1 ('The answer is %s')
| tap (log) // outputs 'The answer is 1', 'The answer is NOTHING', ...
```
… then we remove the `x => x` and change all the `|` to `>>`
```javascript
const add1IfYouCan =
ifOk (add1, 'nothing' | always) // (1)
>> String // (2)
>> dot ('toUpperCase') // (3)
>> sprintf1 ('The answer is %s') // (4)
>> tap (log) // (5)
```
The following pattern always holds:
```javascript
a | b | ... | n = a | (b >> ... >> n)
```
So when the chains start to get long (as above), you can cut pieces out
using this property. For example, you can refactor lines 2-4 into a new
function:
```javascript
// --- convert input to String, make uppercase, perform sprintf.
const processString = String
>> dot ('toUpperCase')
>> sprintf1 ('The answer is %s')
```
And splice it back in:
```javascript
const add1IfYouCan = x => x
| ifOk (add1, 'nothing' | always)
| processString
| tap (log)
```
Or
```javascript
const add1IfYouCan =
ifOk (add1, 'nothing' | always)
>> processString
>> tap (log)
```
## ٭ compositional predicates ٭
`ifOk` is a convenience for `ifPredicate (ok)` or `ok | ifPredicate`.
There is also a 'when' form, which has no 'else' branch.
```javascript
import { add, whenOk, } from 'stick-js'
const add1 = 1 | add // or add (1)
3 | whenOk (add1) // 4
null | whenOk (add1) // undefined
```
The selection of `if` and `when` functions we provide is intentionally skimpy, to encourage you to compose your own.
```javascript
const { floor, } = Math
// --- predicate to match integers.
const isInteger = x => x === floor (x)
// --- or how about
// import { eq, } from 'stick-js'
// const isInteger = x => x | floor | eq (x)
// --- or if you're getting bored:
// import { timesV, asteriskN, passToN, } from 'stick-js'
// const arrowSnd = f => timesV (2) >> asteriskN ([id, f])
// const isInteger = arrowSnd (floor) >> passToN (eq)
// --- now compose it into an anaphoric if:
const ifInteger = isInteger | ifPredicate
// --- now use it:
const add1 = add (1)
; [3.5, 4, 4.2]
| map (ifInteger (add1, 'nothing' | always))
// ['nothing', 5, 'nothing']
```
More complicated predicates:
```javascript
const both = (f, g) => againstAll ([f, g])
const isOdd = x => x % 2
const isOddInteger = both (isInteger, isOdd)
const ifOddInteger = isOddInteger | ifPredicate
; [3.5, 4, 5, 5.5]
| map (ifOddInteger (
add1,
'nothing' | always,
))
// ['nothing', 'nothing', 6, 'nothing']
; [3.5, 4, 5, 5.5] | map (isOddInteger)
// [false, false, 1, false]
```
Note that `againstAll` returns the return value of the last matching
function, hence `1` in the last example. This is because it can be useful to
have that value, and it's analogous to how the short-circuit `&&` works.
To convert a truthy expression to a strict one: simply compose it with the
type constructor `Boolean`.
```javascript
const both = (f, g) => againstAll ([f, g]) >> Boolean
```
Or, you could add `=== true` to the end of the `isOdd` function.
Note that `ifPredicate` and `whenPredicate` match on truthiness, not strict
truth. Rationale: that is how the native `filter` works, and JS's `if`
operator, and many other tools in e.g. Ramda. And converting truthy to
strict Boolean is trivial.
## ٭ compositional decoration ٭
Our `map` function is capped at one argument, meaning the map routine only
gets the value and not the index or the collection.
```javascript
import { map, addIndex, addCollection, } from 'stick-js'
; [4, 5, 6]
| map ((x, idx) => idx) // [undefined, undefined, undefined]
```
But:
```javascript
const mapWithIndex = map | addIndex
const mapWithCollection = map | addCollection
; [4, 5, 6]
| mapWithIndex ((x, idx) => idx) // [0, 1, 2]
; [4, 5, 6]
| mapWithCollection ((x, coll) => coll) // [[4, 5, 6], [4, 5, 6], [4, 5, 6]]
; [4, 5, 6]
| (map | addIndex | addCollection) ((x, idx, coll) => ...)
; [4, 5, 6]
| (map | addCollection | addIndex) ((x, coll, idx) => ...)
```
We can also enhance our merge functions, to deal with conflicts:
```javascript
import { mergeWith, mergeToSym, } from 'stick-js'
const chooseTgt = (src, tgt) => tgt
const chooseSrc = (src, tgt) => src
// --- choose target value on conflict
const mergeChooseTgt = mergeToSym | mergeWith (chooseTgt)
// --- choose source value on conflict
const mergeChooseSrc = mergeToSym | mergeWith (chooseSrc)
const os = { name: 'source name', }
const ot = { name: 'target name', }
os | mergeChooseSrc (ot) // 'source name'
os | mergeChooseTgt (ot) // 'target name'
```
Or to only merge if certain conditions hold:
```javascript
import { mergeWhen, eq, } from 'stick-js'
const { floor, } = Math
const isInteger = x => x | floor | eq (x)
const srcIsInteger = (src, tgt) => src | isInteger
const mergeToWhenSrcIsInteger = mergeToSym | mergeWhen (srcIsInteger)
const os = { val: 2.2, vil: 3, vol: 3.5, }
const ot = { val: 25, vil: 25, vol: 25, vel: 42, }
os | mergeTo (ot) // { val: 2.2, vil: 3, vol: 3.5, vel: 42, }
os | mergeToWhenSrcIsInteger (ot) // { val: 25, vil: 3, vol: 25, vel: 42, }
```
## ٭ semantics and argument order are based where possible on English grammar ٭
(We're dying to see a port to Hungarian, too)
Once you master this, the usage becomes intuitive and greatly reduces the
need to look things up.
(Motivation: what's the argument order of `append`, `times`, and `subtract` in your favorite library?)
```javascript
import {
sprintfN, sprintf1, mergeTo, merge, prependTo, prepend,
appendTo, append, bindPropTo, bindProp, bindTo, bind, invoke,
} from 'stick-js'
const tgt = { thing: 'sandwich', want: 'no thanks', }
const src = { want: 'yes please', }
```
When a function name ends in a preposition, for example, ‘To’, the identifier to
the right is the object of the preposition.
Read this as ‘merge src **to tgt**’
```javascript
src | mergeTo (tgt) // { thing: 'sandwich', want: 'yes please', }
```
The same function name without the preposition means that the identifier to the right is the object of the verb ‘merge’.
Read this as: ‘**merge src** to tgt’
```javascript
tgt | merge (src) // { thing: 'sandwich', want: 'yes please', }
4 | appendTo ([1, 2, 3]) // [1, 2, 3, 4]
; ([1, 2, 3]) | append (4) // [1, 2, 3, 4]
0 | prependTo ([1, 2, 3]) // [0, 1, 2, 3]
; ([1, 2, 3]) | prepend (0) // [0, 1, 2, 3]
const dog = {
name: 'Caesar',
speak () { return 'My name is ' + this.name },
}
const cat = {
name: 'Fritz',
speak () { throw new Error },
}
const f = 'speak' | bindPropTo (dog)
f () // 'My name is Caesar'
// --- 'bind prop "speak" to object'
'speak' | bindPropTo (dog) | invoke // same
// --- also 'bind prop "speak" to object'
dog | bindProp ('speak') | invoke // same
dog.speak | bindTo (dog) | invoke // same
// cat.speak () // Error
dog.speak | bindTo (cat) | invoke // 'My name is Fritz'
cat | bind (dog.speak) | invoke // 'My name is Fritz'
// --- 'call this function on this context', i.e., bind and call.
; ({}.toString) | callOn (3) // '[object Number]'
// --- 'provide this context to this function'
; 3 | provideTo ({}.toString) // '[object Number]'
```
Note that this fits the `a | b | c` pattern:
```javascript
dog.speak | bindTo (cat) | invoke // 'My name is Fritz'
```
So we can also write it as `a | (b >> c)`:
```javascript
dog.speak | (bindTo (cat) >> invoke) // 'My name is Fritz'
```
In fact `bindTo (x) >> invoke` is already provided under the name `callOn`,
and its inverse `provideTo`:
```javascript
dog.speak | callOn (cat) // 'My name is Fritz'
cat | provideTo (dog.speak) // 'My name is Fritz'
```
Some other miscellaneous examples.
```javascript
// --- '3 to the 4th'
3 | toThe (4) // 81
// --- 'divide 3 by 6' or '3 divided by 6'
3 | divideBy (6) // 0.5
// --- 'divide 3 into 6'
3 | divideInto (6) // 2
// --- 'subtract 3 from 4'
3 | subtractFrom (4) // 1
// --- 'subtract 4 from 3'
3 | subtract (4) // -1
// --- '3 minus 4'
3 | minus (4) // -1
```
## ٭ side effects & chaining ٭ mutable vs immutable ٭
```javascript
import {
map, side1, appendM, append, prependM, prepend,
} from 'stick-js'
```
Chaining with the `.` will often not do what you want.
```javascript
; [2, 3, 4]
.push (5)
.unshift (1) // error, return value of previous line was 5
```
But this will:
```javascript
const push = 'push' | side1
const unshift = 'unshift' | side1
```
The 1 in side1 refers to the arity of the function, i.e., the exact number
of arguments it expects. When working with functions in this way it's
important to specify this.
`.push` and `.unshift`, both methods of `Array.prototype`, expect exactly
one argument, hence `side1` in both cases.
```javascript
; [2, 3, 4]
| push (5)
| unshift (1) // [1, 2, 3, 4, 5]
// --- using stick functions for mutable data:
; [2, 3, 4]
| appendM (5)
| prependM (1)
// --- using stick functions for immutable data:
; [2, 3, 4]
| append (5) // new array [2, 3, 4, 5]
| prepend (1) // new array [1, 2, 3, 4, 5]
```
You can insert `tap` anywhere in the chain, which is guaranteed not to mess
with the pipeline no matter what it returns:
```javascript
const double = x => x * 2
; [1, 2]
| map (double) // [2, 4]
| tap (x => console.log (x)) // still [2, 4], performs side-effect (printing)
| tap (x => [100, 200]) // still [2, 4], for demonstration only
| join (',')
// '2,4'
```
Without the first `tap`, this would have been an error, because
`console.log` returns `undefined`.
`tap` is useful to signal the intention of performing side effects or IO. It
is also really useful for debugging. And our `side` family of functions use
`tap` under the hood.
## ٭ factory ٭ synopsis ٭
```javascript
import {
factory, factoryProps,
factoryStatics,
} from 'stick-js'
// --- dog.js:
const proto = {
init () {
...
return this
},
breathe () { return 'huff' },
whoami () { return this.name },
getType () { return this.type },
}
const props = {
type: 'dog',
name: undefined,
}
// --- basic:
export default proto | factory
// --- variants:
export default proto | factory | factoryProps (props)
export default proto | factory | factoryStatics ({ ... }) | factoryProps (props)
// ------ main.js
import Dog from './dog'
const dog = Dog.create ()
// const dog = Dog.create ().init () // useful in practice
dog.breathe () // 'huff'
dog.type // 'dog', if `factoryProps` used
dog.getType () // 'dog', same
dog.whoami () // undefined, because no default.
const dog2 = Dog.create ({ name: 'garfunkel', })
dog2.whoami () // 'garfunkel', thanks to args to create
```
## ٭ factory ٭ with mixins ٭ synopsis ٭
```javascript
// ------ animal.js:
import {
factory, factoryStatics, mixinM, mixinPreM,
factoryProps,
ifPredicate,
} from 'stick-js'
const isOdd = x => x % 2 !== 0
const ifOdd = isOdd | ifPredicate
// --- a 'base' object (animal)
const proto = {
init () {
...
return this
},
move () {
return this.numLegs | ifOdd (
_ => 'hobble',
_ => 'gait',
)
},
breathe () { return 'huff' },
speak () { 'not implemented' | die },
getType () { return this.type },
}
const props = {
type: 'animal',
numLegs: undefined,
}
export default proto | factory | factoryProps (props)
// ------ cheater.js:
// --- some orthogonal functionality
const proto = {
cheat: howMuch => 'I cheat ' + howMuch,
}
export default proto | factory
// ------ dog.js:
import {
sprintf1,
factory,
} from 'stick-js'
import Animal from './animal'
import Cheater from './cheater'
const { proto: animalProto, } = Animal
const { proto: cheaterProto, } = Cheater
// --- a composite object (dog), extended from animal, with extra functions mixed in.
const proto = {
init () {
...
return this
},
speak () { return this.name | sprintf1 ('Dog %s says woof') }
}
const props = {
type: 'dog',
numLegs: 4,
name: undefined,
}
export default proto
| mixinPreM (animalProto)
| mixinM (cheaterProto)
| factory
| factoryProps (props)
// ------ main.js
import Dog from './dog'
const dog = Dog.create ({ name: 'garfunkel', })
// const dog = Dog.create ({ name: 'garfunkel', }).init () // with init
dog.breathe () // 'huff' (from animal)
dog.getType () // 'dog' (function from animal, property from dog)
dog.speak () // 'Dog garfunkel says woof' (function from dog, property from instance initialisation)
dog.cheat ('a bit') // 'I cheat a bit' (from cheater)
```
Stick idioms:
```javascript
const breathe = dot ('breathe')
const getType = dot ('getType')
const speak = dot ('speak')
const cheat = dot1 ('cheat')
const init = side ('init')
const create = dot1 ('create')
const dog = Dog
| create ({ name: 'garfunkel', })
| init
dog | breathe
dog | getType
dog | speak
Dog | create ({ name: 'garfunkel', })
| init
| cheat
```
## ٭ factory ٭ explained
We provide a functional style for working with objects the way JS was
designed to: using prototypical inheritance and Object.create. We hope to
show you that the `new` keyword and 'classes' and all the baggage they bring
are unnecessary, and that they obfuscate the way that it actually works.
We provide a simple abstraction for factories, with as little sugar
and magic as possible, and encourage you to mix and match the components to
do exactly what you need.
To recap: you create an object in JS by first building a prototype object,
consisting of only functions.
```javascript
const animalProto = {
breathe () { return 'huff' },
speak () { throw new Error },
...
}
```
To make an `animal` instance, you pass this prototype object to `Object.create`,
then assign some methods and/or properties. If you wish you can use
`Object.create` again (and again) before assigning properties, copy in some
more values, use `Object.create` again, and so on.
(Nothing will stop you from using `Object.create` on an object with
non-method properties in it, and it will probably do what you want, but it's
best avoided -- add your properties to the last object in the chain).
We encapsulate this process with the notion of a factory, which is an object
which knows how to spawn objects of a certain sort.
```javascript
import { factory, } from 'stick-js'
const animalProto = {
breathe () { return 'huff' },
speak () { throw new Error },
...
}
const Animal = animalProto | factory // other idioms might call it `animal` or `animalFactory`
const animal1 = Animal.create ()
const animal2 = Animal.create ()
animal1.breathe () // 'huff'
animal2.breathe () // 'huff'
animal2.speak () // Error
```
To add properties:
```javascript
const animalProps = {
type: 'animal',
size: undefined,
numLegs: undefined,
}
```
And we recommend always having an `init` method, which you will almost certainly
need. `myFactory.create ().init ()` becomes a well-worn pattern.
```javascript
const isOdd = x => x % 2 !== 0
const ifOdd = isOdd | ifPredicate
const animalProto = {
init () {
...
return this
},
breathe () { return 'huff' },
speak () { throw new Error },
move () {
return this.numLegs | ifOdd (
_ => 'hobble',
_ => 'gait',
)
},
}
const Animal = animalProto | factory | factoryProps (animalProps)
```
On `create`, the properties which are 'ok' will get copied in to the new
object.
The others are there for documentation: put them here, not peppered
throughout the methods. Do use `undefined` for props that are waiting to be
defined, which is arguably better than `null` and definitely better than
`false`. Use `void 8` or your very own favorite number to impress … no one.
```javascript
const animal = Animal.create ().init ()
animal.type // 'animal'
animal.size // undefined
```
You can pass an object to `create` to initialise properties. These will be
copied in *after* the props that were passed to `factoryProps` are.
```javascript
const bigBiped = Animal.create ({ size: 'big', numLegs: 2, }).init ()
```
If for some reason you need access to your newly minted object before `create` is called, you can
use `factoryInit` instead of `factory`. This is how `factoryProps` is implemented internally.
You can also eliminate the dots entirely:
```javascript
const create = dot1 ('create')
const init = side ('init')
const move = dot ('move')
const speak = dot ('speak')
Animal | create ({ size: 'small', numLegs: 2, })
| init
| move // 'gait'
```
Note that by using `side` for `init` we are assured that the instance is
passed down through the pipe and not the return value of `init` (although in
this case, `init` returns `this`, so `dot` would have worked too.) `create`
and `move` definitely need `dot` and not `side`.
To extend `Animal` to the obligatory `Dog` find the `Animal` prototype
(if `animalProto` is not in scope).
```javascript
const animalProto = Animal.proto
```
Or
```javascript
const animalProto = Animal.create ().__proto__ // might not be available in all runtimes
```
Create it, add dog methods, and make a new factory:
```javascript
const dogProto = animalProto
| Object.create
| mergeM ({
speak () { return this.loud ? 'WOOF' : 'woof' },
})
const dogProps = {
numLegs: 4,
loud: undefined,
}
const Dog = dogProto | factory | factoryProps (dogProps)
; [true, false]
| map ((isLoud) => Dog
| create ({ loud: isLoud, })
| (dog => [dog.speak (), dog.breathe (), dog.move ()])
)
// [['WOOF', 'huff', 'gait'], ['woof', 'huff', 'gait']]
```
Note that we can call methods of both `Animal` and `Dog` now.
## ٭ factory ٭ with mixins ٭ explained ٭
```javascript
const Dog = dogProto | mixinM (animalProto) | factory | factoryProps (dogProps)
```
Working with mixins is tricky. At some point, there will be namespace
conflicts and it's not always obvious which version should win out -- and
you have to decide how you want to deal with that.
But you have all the tools now to specify exactly how you want it to work.
```javascript
const Dog = dogProto | mixinPreM (animalProto) | factory | factoryProps (dogProps)
const dog1 = Dog | create ({})
dog1 | breathe // 'huff'
const Dog = dogProto | mixinPreM (animalProto) | factory | factoryProps (dogProps)
Dog.create ({}) | speak // 'woof'
```
We mixed the animal into the dog as a 'pre' mixin, meaning that on name
conflicts, Dog's version will win. If we had used `mixinM` instead of
`mixinPreM`:
```javascript
const Dog = dogProto | mixinM (animalProto) | factory | factoryProps (dogProps)
Dog | create ({})
| speak // Error, this is Animal's version.
```
Non-pre mixins are useful for orthogonal functionality -- something like
logging, for example.
You can add as many pre and post mixins as you like.
```javascript
dogProto | mixinPreM (...) | ... | mixinM (...) | ... | factory
```
Or use the 'N' versions to provide an array:
```javascript
dogProto | mixinPreNM ([a, b, c]) | mixinNM ([d, e, f]) | factory
```
Note the 'M' marker on the mixin functions. This is to make clear that these
functions mutate the prototype object, which might be a bit surprising when
using this style.
We do not provide non-M versions of the mixin functions, because it's not obvious exactly what the
semantics should be, as several alternatives could be equally intuitive: should the prototype chain
be flattened? should it be discarded, leaving only the own keys? should it create a new object using
and mutate it?
You can specify these behaviors explicitly:
```javascript
dogProto | flattenPrototype | <mixin functions ...> |
dogProto | discardPrototype | <mixin functions ...> |
dogProto | Object.create | <mixin functions ...> |
```
## ٭ let expressions ٭
You can consolidate a number of assignment statements into a single let
expression, and also limit the scope of the assignments in a way which is
easy to read. Code which is based on expressions rather than blocks of
statements can be made referentially transparent, and therefore much easier
to read, refactor, & prove the correctness of, especially considering that
each statement is a possible side-effect inducing timebomb.
```javascript
// --- convert a celsius value to both fahrenheit & kelvin.
const convertCelsius = (c) => letV (
c / 5 * 9 + 32, // fahrenheit
c - 273, // kelvin
(fah, kel) => [fah, kel],
)
```
`letV` takes an arbitrary number of values, and expects the last one to be a
function. It simply passes the values in order to the function.
There is an 'N' form (`letVN`, which takes an array of values and one
function).
Far more useful is `lets` and `letS`. (Think `let*` in racket).
If we were to go the other way, from Fahrenheit to Celsius & Kelvin:
```javascript
// --- convert fahrenheit to celsius & kelvin
const convertFahrenheit = (f) => letV (
(f - 32) / 9 * 5, // celsius
(f - 32) / 9 * 5 + 273 // kelvin
(cel, kel) => [cel, kel],
)
```
We see that we are wasting work in the second line, because with `letV` there is no way to capture that
intermediate expression to avoid calculating it again. With `lets`, however,
there is:
```javascript
// --- convert fahrenheit to celsius & kelvin
const convertFahrenheit = fah => lets (
_ => (fah - 32) / 9 * 5, // (1) celsius
(cel) => cel + 273, // (2) kelvin
(cel, kel) => [cel, kel], // (3) result
)
```
`lets` expects each line to be a function. The first line is called with no
argument. The result (1) is passed as the argument to (2). The result of (1)
and the result of (2) are passed as the arguments to (3), and so on. The
result of the expression is the result of the last function.
(Implementation detail: `lets` and `letS` will remain fast with up to 6 arguments. After that we use a slower,
more generic algorithm.)
And of course there is a stick version of `lets` called `letS`. Think of the
'S' marker as 'stick enabled' and the 's' as 'stick disabled'. `letS`
expects a value to be piped in. *Note*: the 'S' marker implies the 'N'
marker: the arguments must be an array, or else it would be impossible to
curry.
```javascript
const convertFahrenheit = fah => fah | letS ([
(fah) => (fah - 32) / 9 * 5, // (1) celsius
(fah, cel) => cel + 273, // (2) kelvin
(fah, cel, kel) => [cel, kel] // (3) result
])
```
This wouldn't be the most natural use of `letS`, but it shows how it works:
Function (1) receives as a single argument the piped in value (`fah` in this case).
Function (2) receives `fah`, and the result of (1). (3) receives `fah`, the
results of (1) and (2), and so on.
By now we know that we can remove `fah => fah |` from the first line to
make it point-free, and we can use underscores to indicate ignored values:
```javascript
const convertFahrenheit = letS ([
(fah) => (fah - 32) / 9 * 5,
(_, cel) => cel + 273,
(_, cel, kel) => [cel, kel],
])
```
As an exercise we could try to make the entire expression as point-free as
possible, at the possible expense of everyone's sanity:
```javascript
import { letS, minus, divideBy, multiply, add, arg1, list, tail, } from 'stick-js'
const convertFahrenheit = letS ([
minus (32) >> divideBy (9) >> multiply (5),
arg1 >> add (273),
list >> tail,
])
convertFahrenheit (86) // [30, 303]
```
## ٭ exceptions ٭ try/catch ٭
Note: `decorateException` has been removed -- use `decorateRejection` from
alleycat-js instead.
```javascript
import { tryCatch, } from 'stick-js'
const dubiousFunction = ...
dubiousFunction | tryCatch (
// --- no exception: `v` is the return value of `dubiousFunction`
(v) => ...,
// --- exception thrown: `e` is the exception
(e) => e | decorateException ('Dubious function said:')
)
```
Sometimes it's nice to rethrow an exception, with a string prefixed to it so
you can tell what went wrong:
```javascript
(e) => e | decorateException ('Dubious function said:') | die
const throwError = reason => reason
| exception // new Error (reason)
| raise // throw it
const throwError = reason => reason | (exception >> raise)
// `exception >> raise` is also known as `die`
```
Note that this is illegal in JS, because `throw` is not an expression:
```javascript
const throwError = reason => throw new Error (reason)
```
But we can trick it like this:
```javascript
const throwError = reason => reason | die
// or just
// const throwError = die
```
Now we can improve this common code:
```javascript
let answer
try {
answer = dubiousFunction () + 10
} catch (e) {
console.warn (e)
answer = 'bad news'
}
```
like this:
```javascript
const answer = dubiousFunction | tryCatch (
plus (10),
(e) => {
e | decorateException ("That didn't go well")
| console.warn
return 'bad news'
},
}
```
Or perhaps:
```javascript
const answer = dubiousFunction | tryCatch (
plus (10),
decorateException ("That didn't go well")
>> tap (console.warn)
>> ('bad news' | always)
)
```
## ٭ cond ٭
```javascript
import {
cond, condN, condS, guard, guardV, sprintf1, otherwise,
} from 'stick-js'
```
Naive form:
```javascript
cond (
[() => 3 === 4, () => 'strange'],
[() => 3 === 5, () => 'even stranger'],
[() => true, () => 'ok'],
) // 'ok'
```
`() => true` is the fallback case and can also be written `T`.
An arbitrary number of lines can be provided.
Using `guard` and `otherwise`:
```javascript
cond (
(() => 3 === 4) | guard (() => 'strange'),
(() => 3 === 5) | guard (() => 'even stranger'),
otherwise | guard (() => 'ok'),
) // 'ok'
```
If the guard functions return simple expressions, `guardV` can be more
convenient:
```javascript
cond (
(() => 3 === 4) | guardV ('strange'),
(() => 3 === 5) | guardV ('even stranger'),
otherwise | guardV ('ok'),
)
```
The most useful version is `condS`. Remember, 'S' implies 'N', so give it an
array.
```javascript
const checkVal = val => val | condS ([
eq (4) | guard (val => val | sprintf1 ('%s was 4')),
lt (4) | guard (val => val | sprintf1 ('%s was less than 4')),
gt (4) | guard (val => val | sprintf1 ('%s was more than 4')),
otherwise | guardV ("error, this shouldn't happen"),
])
```
Cleaning it up a bit, and inverting the parenthetical test expression to use
sticks:
```javascript
const checkVal = condS ([
4 | eq | guard (sprintf1 ('%s was 4')),
4 | lt | guard (sprintf1 ('%s was less than 4')),
4 | gt | guard (sprintf1 ('%s was more than 4')),
otherwise | guardV ("error, this shouldn't happen"),
])
```
Wut?! This does work, strange as it looks. Try it for yourself:
```javascript
; [3, 4, 5, {}]
| map (checkVal)
| join (' | ')
// 3 was less than 4 | 4 was 4 | 5 was more than 4 | error, this shouldn't happen
```
## ٭ extended regexes ٭
We provide regex functions which fit the idiom and mostly have an 'X' in the
name (think /x if you know Perl).
These are 'extended regexes', in which whitespace is ignored. Note that we
do not skip comments in the regex, only whitespace, even inside character
classes. Use `\s` if you really want to match on whitespace.
```javascript
'egbert' | xMatch (/ (eg) (..) [rs] /)
// [ 'egber', 'eg', 'be', index: 0, input: 'egbert' ]
// --- same, using a regex string instead of a RegExp literal.
'egbert' | xMatchStr (' (eg) (..) [rs] ')
// [ 'egber', 'eg', 'be', index: 0, input: 'egbert' ]
const vowels = []
const mapper = appendToM (vowels)
'egbert druppelvanger' | xMatchGlobal (/ [ae] /) (mapper)
vowels // [ 'e', 'e', 'e', 'a', 'e' ]
'egbert\ndruppelvanger' | xMatchStrFlags (' ^ d ') ('s')
// null
'egbert\ndruppelvanger' | xMatchStrFlags (' ^ d ') ('m')
// [ 'd', index: 7, input: 'egbert\ndruppelvanger' ]
const toUpper = dot ('toUpperCase')
const ifReplaceVowels = ifXReplace (/ ([aeiou]) /) ('x')
'egbert' | ifReplaceVowels (toUpper) ('bad' | always)
// XGBXRT
const ifReplaceVowelsGlobal = ifXReplace (/ ([aeiou]) /g) ('x')
'egbert' | ifReplaceVowelsGlobal (toUpper) ('bad' | always)
// XGBXRT
// --- same, using a regex string instead of a RegExp literal.
const ifReplaceVowelsGlobalAlt = ifXReplaceStrFlags (' ([aeiou]) ') ('g') ('x')
'egbert' | ifReplaceVowelsGlobalAlt (toUpper) ('bad' | always)
// XGBXRT
```
## ٭ all ٭ any ٭
If you're like the author you have a hard time remembering how Ramda's
`all`, `any`, `both`, `either`, `and`, `or`, `anyPass`, and so on work. Does
the 'all' refer to all the functions? or all the values? and so on.
Our solution to making the semantics clearer and enriching the functionality
of these functions centers around the preposition 'against': you match
a value *against* a predicate.
```javascript
const isOdd = x => isInteger (x) && (x % 2 !== 0)
const isLt3 = 3 | lt
// --- return `true` if input is truthy, `false` otherwise.
const truthy = id
// const truthy = Boolean // also works
```
Match *all* values *against* the predicate `isOdd`
```javascript
; [1, 2, 3, 4, 5] | allAgainst (isOdd) // false
; [1, 3, 5] | allAgainst (isOdd) // true
```
Match *all* values *against* the predicate `truthy`
```javascript
const allTruthy = allAgainst (truthy)
; [1, 2, 3, 4, 5, null] | allTruthy // false
; [1, 2, 3, 4, 5] | allTruthy // true
```
The value matches *all* the predicates
```javascript
const isOddAndLt3 = againstAll ([isOdd, isLt3])
1 | isOddAndLt3 // true
1.1 | isOddAndLt3 // false
2 | isOddAndLt3 // false
3 | isOddAndLt3 // false
```
The value matches *any* of the predicates
```javascript
const isOddOrLt3 = againstAny ([isOdd, isLt3])
2 | isOddOrLt3 // true
3 | isOddOrLt3 // true
4 | isOddOrLt3 // false
5 | isOddOrLt3 // true
```
We can also match multiple values against multiple predicates, in various
combinations of 'all' and 'any' (Ramda doesn't have this out of the box --
corrections welcome)
All the values are odd and less than 3.
```javascript
const allOddAndLt3 = allAgainst (isOddAndLt3)
; [1, 2, 3, 4, 5] | allOddAndLt3 // false
; [1, 3, 5] | allOddAndLt3 // true
```
All the values are odd and less than 3.
```javascript
const allOddOrLt3 = allAgainst (isOddOrLt3)
; [1, 2, 3, 4, 5] | allOddOrLt3 // false
; [2, 3, 5] | allOddOrLt3 // true
```
Any of the vales are odd and less than 3.
```javascript
const anyOddAndLt3 = anyAgainst (isOddAndLt3)
; [3, 4, 5] | anyOddAndLt3 // false
; [2, 4, 5] | anyOddAndLt3 // false
; [1, 4, 5] | anyOddAndLt3 // true
```
Any of the vales are odd or less than 3.
```javascript
const anyOddOrLt3 = anyAgainst (isOddOrLt3)
; [4, 6] | anyOddOrLt3 // false
; [3, 6] | anyOddOrLt3 // true
; [3, 5] | anyOddOrLt3 // true
; [2, 6] | anyOddOrLt3 // true
; [1, 6] | anyOddOrLt3 // true
```
Both values are truthy / either value is odd. Note that these forms take
spread out arguments, not an array (so they are sugar for `allAgainst` /
`anyAgainst`)
```javascript
const bothTruthy = bothAgainst (truthy)
const eitherOdd = eitherAgainst (isOdd)
const isOddAndLt3Alt = againstBoth (isOdd) (isLt3)
const isOddOrLt3Alt = againstEither (isOdd) (isLt3)
// --- i.e. ; [null, 3] | allTruthy
bothTruthy (null, 3) // false
bothTruthy (1, 3) // 3
eitherOdd (1, 2) // true
eitherOdd (null, 2) // false
eitherOdd (2, 4) // false
1 | isOddAndLt3Alt // true
1.1 | isOddAndLt3Alt // false
2 | isOddAndLt3Alt // false
3 | isOddAndLt3Alt // false
2 | isOddOrLt3Alt // true
3 | isOddOrLt3Alt // true
4 | isOddOrLt3Alt // false
5 | isOddOrLt3Alt // true
```
## ٭ merging ٭
We provide 8 basic merge functions, corresponding to all combinations of
three binary choices:
1. *to* vs. *from* (order of arguments)
1. '*own*' vs. '*in*' (prototype values)
1. *mutable* vs. *immutable* (whether to clone the target first)
There are a few conventions to keep in mind when trying to understand the
semantics. *To* vs. *from* is just a question of switching the arguments, so we
don't need to discuss it, but the other 4 combinations have some caveats.
```javascript
tgt | merge (src) // (1) own, immutable
tgt | mergeIn (src) // (2) in, immutable
tgt | mergeM (src) // (3) own, mutable
tgt | mergeInM (src) // (4) in, mutable
// mergeTo, mergeInTo, mergeToM, mergeInToM: 'to' forms
```
In the immutable cases, a shallow clone is made of the target before
merging. You must keep in mind whether the clone will use only 'own'
properties or also the prototype ('in') properties. Case (1) corresponds to
'own' and case (2) to 'in'.
No matter which it is, all copied properties will become own properties of
the clone.\*
Then, properties are copied over from the source: only own properties in
case (1), also 'in' properties in case (2).
The rule to remember in the immutable case is: 'own' merges with 'own' and
'in' with 'in'.
In the mutable case, properties are copied to the target from the source, and the target is not
cloned or altered in any other way. In particular this means that the 'own' in case (3) only applies
to the source: the prototype chain of the target will not be flattened or discarded or altered at
all.
The rule to remember in the mutable case is: the target is never altered in
any way, besides having properties copied in. So the 'own' / 'in'
distinction only applies to the source.
We feel that these conventions are the most straightforward and lead to
easily inferrable behavior.
In all cases, we are only ever talking about enumerable properties.
\* This is good enough for most styles of functional programming in JS, many
of which don't even bother with the distinction. People often expect
shallow, cheap objects when programming this way. If you absolutely must
preserve the prototype chain intact, you'll have to find a way to clone it
yourself, then use the 'M' versions of our merge functions on your cloned
object. You might also get away with using `Object.create` on it first,
which will of course create one more link in the prototype chain.
## ٭ frontend stuff ٭
```javascript
import { path, prop, whenTrue, always, } from 'stick-js'
```
If you use react/redux, perhaps with saga, chances are your modules end in
something like this:
```javascript
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'home', reducer });
const withSaga = injectSaga({ key: 'home', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(HomePage);
```
We should see by now that this composing of functions, invoked upon a single
value, is exactly our pipe pattern. So why not:
```javascript
export default HomePage
| connect (mapStateToProps, mapDispatchToProps)
| injectSaga ({ key: 'home', saga, })
| injectReducer ({ key: 'home', reducer, })
```
And maybe you call actions using a structure like:
```javascript
export function mapDispatchToProps(dispatch) {
return {
onChangeUsername: (evt) => dispatch(changeUsername(evt.target.value)),
};
};
```
Why not:
```javascript
export const mapDispatchToProps = (dispatch) => ({
onChangeUsername: path (['target', 'value'] >> changeUsername >> dispatch,
})
```
Try it yourself and see :D
If you use `styled components`, perhaps you pass optional props in. Checking
for the presence of the props can be annoying, so how about:
```javascript
const SomeElementS = styled.div`
top: 5%;
left: 5%;
${ prop ('width') >> whenOk (sprintf1 ('width: %spx;')) }
${ prop ('height') >> whenOk (sprintf1 ('height: %spx;')) }
${ prop ('error') >> whenTrue ('color: red;' | always) }
`
```
<SomeElementS width='100%' error={true} />
If you use `saga`, perhaps you have something like this:
```javascript
export function* getRepos() {
const username = yield select(makeSelectUsername());
const requestURL = `https://api.github.com/users/${username}/repos`;
try {
const repos = yield call(request, requestURL);
yiel