https://github.com/andrejewski/affection
Wish I had some..declarative side-effects
https://github.com/andrejewski/affection
data-driven declarative side-effects
Last synced: 4 months ago
JSON representation
Wish I had some..declarative side-effects
- Host: GitHub
- URL: https://github.com/andrejewski/affection
- Owner: andrejewski
- License: mit
- Created: 2018-09-30T01:27:55.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2020-05-11T17:15:41.000Z (over 5 years ago)
- Last Synced: 2025-04-05T00:02:37.825Z (10 months ago)
- Topics: data-driven, declarative, side-effects
- Language: JavaScript
- Homepage:
- Size: 61.5 KB
- Stars: 40
- Watchers: 4
- Forks: 1
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Affection
> Declarative side-effects
```sh
npm install affection
```
[](https://www.npmjs.com/package/affection)
[](https://travis-ci.org/andrejewski/affection)
[](https://greenkeeper.io/)
Affection is a library for describing side-effects as plain data and providing composition utilities.
This project aims to improve on similar libraries by not using generators.
Generators make testing difficult in that:
- They can have internal state.
- Each segment of the function cannot be tested in isolation.
- Each segment of the function can only be reached after the segments before it.
- Generators are awkward. Conversing with a generator using `next()` isn't as simple as function calling.
- Composition of generators is harder than functions inherently.
So Affection is all about functions, with the goals:
- Improve testability through the use of pure functions.
- Improve code reuse through la-a-carte composition of side-effects.
Let's see how we do.
## Examples
This first example does not use any composition.
```js
import { run, call, callMethod } from 'affection'
const getJSON = url => [
call(fetch, [url]),
resp => [callMethod(resp, 'json')]
]
async function main () {
const payload = await run(getJSON('http://example.com'))
console.log(payload)
}
```
This second example does the same as the first.
Here we are using the composition utilities.
```js
import { step, runStep, batchSteps, call, callMethod } from 'affection'
const fetchUrl = url => call(fetch, [url])
const readJSON = resp => callMethod(resp, 'json')
const getJSON = batchSteps([fetchUrl, readJSON].map(step))
async function main () {
const payload = await runStep(getJSON, 'http://example.com')
console.log(payload)
}
```
## Documentation
The package contains the following:
##### Effects
- [`call(func, args, context)`](#call)
- [`callMethod(obj, method, args)`](#callmethod)
- [`all(effects)`](#all)
- [`race(effects)`](#race)
- [`itself(value)`](#itself)
See [`defaultHandle`](#defaulthandle) for adding more.
##### Execution
- [`run(plan[, handle])`](#run)
##### Composition
- [`step(makeEffect)`](#step)
- [`mapStep(step, transform)`](#mapstep)
- [`batchStep(steps)`](#batchsteps)
- [`runStep(step, input[, handle])`](#runstep)
### `call`
> `call(func: function, args: Array, context: any): Effect`
Describes a function call of `func.apply(context, args)`.
### `callMethod`
> `callMethod(obj: any, method: String, args: Array): Effect`
Describes a method call of `obj[method].apply(obj, args)`
### `all`
> `all(effects: Array): Effect`
Describes combining effects. Like `Promise.all`.
### `race`
> `race(effects: Array): Effect`
Describes racing effects. Like `Promise.race`.
### `itself`
> `itself(value: any): Effect`
Describes a value. This is an identity function for Effects.
### `defaultHandle`
> `defaultHandle(effect: Effect, handle: function): any`
Performs the action described by a particular effect.
`defaultHandle` provides the handling for the effects included in Affection.
To add more, create a new handle that wraps `defaultHandle` and pass that to `run`.
For example, say we want to add a timeout effect:
```js
import { defaultHandle } from 'affection'
export function timeout (duration) {
return { type: 'timeout', duration }
}
export function myHandle (effect, handle) {
if (effect.type === 'timeout') {
return new Promise(resolve => setTimeout(resolve, effect.duration))
}
return defaultHandle(effect, handle)
}
// Later...
async function main () {
await run([timeout(1000)], myHandler)
// Will have waited a second
}
```
### `run`
> `run(plan: [Effect, function?], handle: function = defaultHandle): any`
Executes a plan.
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.
If the function is not provided, execution terminates and the result is returned.
### `step`
> `step(makeEffect: any -> Effect): Step`
Creates a step.
A step is a means of encapsulating an effect without needing a plan (as described by `run`).
This is hard to understand without an understanding of how `run` works.
The `run` function is recursively executing plans until there is nothing more to do.
A step is a way of saying, "Execute this effect; I don't know what happens with the result."
This is for code reuse: effects should be decoupled from their consumers.
For more clarity, let's look at the `step` function:
```js
const step = makeEffect => next => input => [makeEffect(input), next]
```
We define our `makeEffect` without needing to know the consumer.
The `next` is what will consume the result.
Later, steps are composed when the consumers are known.
Finally, the step is given an `input` to build its effect.
In summary, steps decouple:
- Creating the effect: `makeEffect`
- Consuming the effect's result: `next`
- Building the final plan, given an `input`
### `mapStep`
> `mapStep(step: Step, transform: function): Step`
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.
This is good for passing along context without mucking up simple steps.
For example, we are building a dictionary of the most used word for each country.
We want to retain the country we are querying about in the result.
```js
const getMostUsedWordInCountry = country => call(MyAPI, [country])
const countryWordStep = step(getMostUsedWordInCountry)
const getCountryWord = mapStep(countryWordStep, (result, country) => ({ country, word: result }))
runStep(getCountryWord, 'Canada').then(result => {
console.log(result)
// => { country: 'Canada', word: 'Sorry' }
})
```
### `batchSteps`
> `batchSteps(steps: Array): Step`
Creates a new step which will call each step passing the result of first step to the next and so on.
### `runStep`
> `runStep(step: Step, input: any, handle: function = defaultHandle): any`
Executes a `step` with a given `input`.
Uses `run` so `handle` works in the same way.