|
| 1 | +# Affection |
| 2 | +> Declarative side-effects |
| 3 | +
|
| 4 | +```sh |
| 5 | +npm install affection |
| 6 | +``` |
| 7 | + |
| 8 | +Affection is a library for describing side-effects as plain data and providing composition utilities. |
| 9 | +This project aims to improve on similar libraries by not using generators. |
| 10 | + |
| 11 | +Generators make testing difficult in that: |
| 12 | + |
| 13 | +- They can have internal state. |
| 14 | +- Each segment of the function cannot be tested in isolation. |
| 15 | +- Each segment of the function can only be reach after the segments before it. |
| 16 | +- Generators are awkward. Conversing with a generator with `next()` isn't as simple as function calling. |
| 17 | +- Composition of generators is harder than functions inherently. |
| 18 | + |
| 19 | +So Affection is all about functions, with the goals: |
| 20 | + |
| 21 | +- Improve testability through the use of pure functions. |
| 22 | +- Improve code reuse through la-a-carte composition of side-effects. |
| 23 | + |
| 24 | +Let's see how we do. |
| 25 | + |
| 26 | +## Examples |
| 27 | + |
| 28 | +This first example does not use any composition. |
| 29 | + |
| 30 | +```js |
| 31 | +import { run, call, callMethod } from 'affection' |
| 32 | + |
| 33 | +const getJSON = url => [ |
| 34 | + call(fetch, url), |
| 35 | + resp => [callMethod(resp, 'json')] |
| 36 | +] |
| 37 | + |
| 38 | +async function main () { |
| 39 | + const payload = await run(getJSON('http://example.com')) |
| 40 | + console.log(payload) |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +This second example does the same as the first. |
| 45 | +Here we are using the composition utilities. |
| 46 | + |
| 47 | +```js |
| 48 | +import { step, runStep, batchSteps, call, callMethod } from 'affection' |
| 49 | + |
| 50 | +const fetchUrl = url => call(fetch, [url]) |
| 51 | +const readJSON = resp => callMethod(resp, 'json') |
| 52 | +const getJSON = batchSteps([fetchUrl, readJSON].map(step)) |
| 53 | + |
| 54 | +async function main () { |
| 55 | + const payload = await runStep(getJSON, 'http://example.com') |
| 56 | + console.log(payload) |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +## Documentation |
| 61 | +The package contains the following: |
| 62 | + |
| 63 | +##### Effects |
| 64 | +- [`call(func, args, context)`](#call) |
| 65 | +- [`callMethod(obj, method, args)`](#callmethod) |
| 66 | +- [`all(effects)`](#all) |
| 67 | +- [`race(effects)`](#race) |
| 68 | +- [`itself(value)`](#itself) |
| 69 | + |
| 70 | +See [`defaultHandle`](#defaulthandle) for adding more. |
| 71 | + |
| 72 | +##### Execution |
| 73 | +- [`run(plan[, handle])`](#run) |
| 74 | + |
| 75 | +##### Composition |
| 76 | +- [`step(makeEffect)`](#step) |
| 77 | +- [`mapStep(step, transform)`](#mapstep) |
| 78 | +- [`batchStep(steps)`](#batchsteps) |
| 79 | +- [`runStep(step, input[, handle])`](#runstep) |
| 80 | + |
| 81 | +### `call` |
| 82 | +> `call(func: function, args: Array<any>, context: any): Effect` |
| 83 | +
|
| 84 | +Describes a function call of `func.apply(context, args)`. |
| 85 | + |
| 86 | +### `callMethod` |
| 87 | +> `callMethod(obj: any, method: String, args: Array<any>): Effect` |
| 88 | +
|
| 89 | +Describes a method call of `obj[method].apply(obj, args)` |
| 90 | + |
| 91 | +### `all` |
| 92 | +> `all(effects: Array<Effect>): Effect` |
| 93 | +
|
| 94 | +Describes combining effects. Like `Promise.all`. |
| 95 | + |
| 96 | +### `race` |
| 97 | +> `race(effects: Array<Effect>): Effect` |
| 98 | +
|
| 99 | +Describes racing effects. Like `Promise.race`. |
| 100 | + |
| 101 | +### `itself` |
| 102 | +> `itself(value: any): Effect` |
| 103 | +
|
| 104 | +Describes a value. This is an identity function for Effects. |
| 105 | + |
| 106 | +### `defaultHandle` |
| 107 | +> `defaultHandle(effect: Effect, handle: function): any` |
| 108 | +
|
| 109 | +Performs the action described by a particular effect. |
| 110 | +`defaultHandle` provides the handling for the effects included in Affection. |
| 111 | +To add more, create a new handle that wraps `defaultHandle` and pass that to `run`. |
| 112 | + |
| 113 | +For example, say we want to add a timeout effect: |
| 114 | + |
| 115 | +```js |
| 116 | +import { defaultHandle } from 'affection' |
| 117 | + |
| 118 | +export function timeout (duration) { |
| 119 | + return { type: 'timeout', duration } |
| 120 | +} |
| 121 | + |
| 122 | +export function myHandle (effect, handle) { |
| 123 | + if (effect.type === 'timeout') { |
| 124 | + return new Promise(resolve => setTimeout(resolve, effect.duration)) |
| 125 | + } |
| 126 | + return defaultHandle(effect, handle) |
| 127 | +} |
| 128 | + |
| 129 | +// Later... |
| 130 | + |
| 131 | +async function main () { |
| 132 | + await run([timeout(1000)], myHandler) |
| 133 | + // Will have waited a second |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +### `run` |
| 138 | +> `run(plan: [Effect, function?], handle: function = defaultHandle): any` |
| 139 | +
|
| 140 | +Executes a plan. |
| 141 | +A plan is an array where the first element is an Effect to be handled using `handle` and the second element is a function to call with the result of the Effect. |
| 142 | +If the function is not provided, execution terminates and the result is returned. |
| 143 | + |
| 144 | +### `step` |
| 145 | +> `step(makeEffect: any -> Effect): Step` |
| 146 | +
|
| 147 | +Creates a step. |
| 148 | +A step is a means of encapsulating an effect without needing a plan (as described by `run`). |
| 149 | + |
| 150 | +This is hard to understand without an understanding of how `run` works. |
| 151 | +The `run` function is recursively executing plans until there is nothing more to do. |
| 152 | +A step is a way of saying, "Execute this effect; we might be done, might not." |
| 153 | +There could be 5 more effects to run or it's the end result; the step doesn't need to know. |
| 154 | + |
| 155 | +This is for code reuse: effects should be decoupled from their consumers. |
| 156 | + |
| 157 | +### `mapStep` |
| 158 | +> `mapStep(step: Step, transform: function): Step` |
| 159 | +
|
| 160 | +Creates a new step which will return the result of `transform` called with the input to the `step` `makeEffect` and the result of the Effect. |
| 161 | + |
| 162 | +This is good for passing along context without mucking up simple steps. |
| 163 | +For example, we are building a dictionary of the most used word for each country. |
| 164 | +We want to retain the country we are querying about in the result. |
| 165 | + |
| 166 | +```js |
| 167 | +const getMostUsedWordInCountry = country => call(MyAPI, country) |
| 168 | +const countryWordStep = step(getMostUsedWordInCountry) |
| 169 | +const getCountryWord = mapStep(countryWordStep, (result, country) => ({ country, word: result })) |
| 170 | + |
| 171 | +runStep(getCountryWord, 'Canada').then(result => { |
| 172 | + console.log(result) |
| 173 | + // => { country: 'Canada', word: 'Sorry' } |
| 174 | +}) |
| 175 | +``` |
| 176 | + |
| 177 | +### `batchSteps` |
| 178 | +> `batchSteps(steps: Array<Step>): Step` |
| 179 | +
|
| 180 | +Creates a new step which will call each step passing the result of first step to the next and so on. |
| 181 | + |
| 182 | +### `runStep` |
| 183 | +> `runStep(step: Step, input: any, handle: function = defaultHandle): any` |
| 184 | +
|
| 185 | +Executes a `step` with a given `input`. |
| 186 | +Uses `run` so `handle` works in the same way. |
0 commit comments