Comparing version 0.0.191 to 0.0.192
{ | ||
"name": "attain", | ||
"version": "0.0.191", | ||
"version": "0.0.192", | ||
"description": "A library for modelling and accessing data.", | ||
@@ -5,0 +5,0 @@ "main": "dist/attain.min.js", |
747
readme.md
attain | ||
====== | ||
- Streams 🐍 | ||
- Data Modelling and Persistence 💾 | ||
- Sum Types 🐱🏍 | ||
- Queries 👓 | ||
- View Layer 💨 | ||
This libraries optimizes for composition and safety without forgetting that this is ultimately still Javascript. | ||
[attain.harth.io](https://attain.harth.io) | ||
#### Installation | ||
`npm install attain` | ||
#### Quick Start | ||
Queries: | ||
```js | ||
import { $ } from 'attain' | ||
const d = $.a.b.c.d | ||
let state = {} | ||
state = d( 2 ) (state) | ||
state | ||
// => { a: { b: { c: { d: 2} } } } | ||
``` | ||
Sum Types: | ||
```js | ||
import * as A from 'attain' | ||
const User = A.type('User', ['LoggedIn', 'LoggedOut']) | ||
const message = | ||
User.fold({ | ||
LoggedIn: ({ username }) => `Welcome ${username}!`, | ||
LoggedOut: () => 'Bye!' | ||
}) | ||
message ( User.LoggedIn({ username: 'Bernie' }) ) | ||
// => 'Welcome Bernie!' | ||
message ( User.LoggedIn() ) | ||
// => 'Bye!' | ||
``` | ||
#### Either | ||
```js | ||
const success = A.Y('Success') | ||
const failure = A.N('Oh no!') | ||
A.isY(success) | ||
// => true | ||
// Get the value from success or null | ||
A.getOr(null) (success) | ||
// => 'Success' | ||
A.getOr(null) (failure) | ||
// => null | ||
A.getWith(null, x => x => x.toUpperCase()) (success) | ||
// => 'SUCCESS' | ||
A.getOr(null) (failure) | ||
// => null | ||
A.fold({ N: () => false, Y: () => true ) (success) | ||
// => true | ||
A.bifold( () => false, () => true ) (success) | ||
// => true | ||
A.map( x => x.toUpperCase() ) (success) | ||
//=> A.Y('SUCCESS') | ||
A.map( x => x.toUpperCase() ) (failure) | ||
//=> A.N('Oh No!') | ||
``` | ||
API | ||
--- | ||
## Utils | ||
You'll see the usage of some utilities in the example code. Here's a quick breakdown. | ||
#### run | ||
<img src="https://media.giphy.com/media/l0HUjziiiniIsRUY0/giphy.gif"/> | ||
One day in the hopefully not so distant future we'll have a feature in javascript called the pipeline operator. | ||
The pipeline operator allows you to compose and apply functions in one step. It makes for some very nice, reusable and decoupled code. | ||
`run` is our interim solution until `|>` lands. | ||
It simply takes some input data as the first argument, and some functions as the remaining arguments and applies each function in order to the previous result. | ||
```js | ||
run( | ||
2 | ||
, x => x * 2 | ||
) | ||
// => 4 | ||
``` | ||
```js | ||
run( | ||
2 | ||
, x => x * 2 | ||
, x => x + '' | ||
) | ||
// => '4' | ||
``` | ||
This is handy for `attain` because all of our functions are static and do not rely on `this` for context. | ||
Eventually, if/when `|>` lands, `run` will likely be deprecated and replaced and all examples will use `|>` instead. | ||
> 🎁 If you're a adventurous you can try `|>` today via the babel [plugin](https://babeljs.io/docs/en/babel-plugin-proposal-pipeline-operator) | ||
## Queries | ||
What are queries? Well to be honest, they are a thing I made up. | ||
<img src="https://media.giphy.com/media/l4JA1COQqiZB6/giphy.gif"/> | ||
But... they are basically the same thing as [lenses](https://hackage.haskell.org/package/lens-tutorial-1.0.4/docs/Control-Lens-Tutorial.html) but with some JS _magical_ shortcuts. | ||
Lenses can be a little tricky to learn, so think of queries as `jquery` but for your traversing your normal JS object. | ||
#### Quick Start | ||
Here's an example of `$` which creates a querie bound to a store. Sounds fancy! | ||
```js | ||
import { $, Z } from 'attain' | ||
// Create some data | ||
let state = {} | ||
// Create a query bound to state | ||
const $d = Z({ | ||
// where to get the root state | ||
,read: () => state | ||
// Tell's `attain` how to talk to your store | ||
// If this was redux, we might use dispatch here | ||
// But because it's just a simple object | ||
// we mutate it directly after running f(state) | ||
,write: f => state = f(state) | ||
}) | ||
// query into the state and | ||
// create a micro reactive store | ||
// for the path a.b.c.d | ||
.a.b.c.d | ||
// Set some data | ||
$d('hello') | ||
//=> { a: { b: { c: { d: 'hello' } }} } | ||
// Transform some data | ||
$d( x => x.toUpperCase() ) | ||
//=> { a: { b: { c: { d: 'HELLO' } }} } | ||
// Get your data | ||
$d() | ||
//=> 'HELLO' | ||
// Look at the original: | ||
state | ||
//=> { a: { b: { c: { d: 'HELLO' }} } } | ||
// Drop your item from state | ||
$d.$delete() | ||
// { a: { b: { c: {} }}} | ||
$d.$stream.map( | ||
// be notified of changes | ||
d => console.log(d) | ||
) | ||
$d.$removed.map( | ||
// and be notified of deletions | ||
d => console.log(d) | ||
) | ||
``` | ||
`attain` queries are just objects that allow you to get/set/transform a particular location in an object. | ||
Lenses are a far more generic abstraction, but are generally used to do the above, so this library optimizes for that approach. | ||
#### $ | ||
<img src="https://media.giphy.com/media/Hidva3NC6BulW/giphy.gif"/> | ||
`$` lets you get/set and transform a value that you've queries. | ||
```js | ||
const set = | ||
$.a.b.c( 2 ) | ||
const update = | ||
$.a.b.c( x => x * 2 ) | ||
const get = | ||
$.a.b.c() | ||
let state = {} | ||
state = set(state) | ||
// => { a: { b: { c: 2 } } } | ||
state = update(state) | ||
// => { a: { b: { c: 4 } } } | ||
get(state) | ||
// => [4] | ||
``` | ||
Notice `get(state)` returns a list, that's because queries can return _multiple results_. | ||
You can use `.$values` to query against an iterable, or use `$union` to merge two queries into one query. | ||
If you want to use queries but also be notified of changes, check out `Z` | ||
## Stream | ||
Streams are useful for modelling relationships in your business logic. Think of them like excel formulas. A Cell in Excel can be either raw data or a formula that automatically updates the computed value when the underlying data changes. | ||
Streams are incredibly useful for modelling user interfaces because it allows us to create reliable persistent relationships that abstract away the complexities of async event dispatches changing our underlying state. | ||
`attain` uses the `mithril-stream` library which was inspired by the library stream library `flyd`. `attain` adds a few handy composable utils on top of the rather barebones mithril implementation. | ||
#### of | ||
Create a stream | ||
```js | ||
import { stream } from 'attain' | ||
const s = stream(1) | ||
// get most recent value | ||
s() | ||
//=> 1 | ||
// update the value | ||
s(2) | ||
s() | ||
//=> 2 | ||
``` | ||
#### map | ||
`map` allows us to create streams that respond to another streams data changing. Think of them like Excel formulas. | ||
`map` is the __most important__ function streams offer. | ||
```js | ||
import { stream as s } from 'attain' | ||
const cell = s.of(1) | ||
const formula = | ||
s.map ( x => x * 2 ) (cell) | ||
const formula2 = | ||
s.map ( x => x * 3 ) (formula) | ||
formula() // => 2 | ||
formula2() // => 6 | ||
cell(10) | ||
formula() // => 20 | ||
formula2() // => 60 | ||
``` | ||
#### log | ||
`log` allows you to quickly log multiple streams, it takes advantage of object shorthand, internally subscribes to each stream and uses the key as the prefix in the log | ||
```js | ||
s.log({ cell, formula, formula2 }) | ||
``` | ||
Which will log (from the previous example) | ||
``` | ||
cell: 1 | ||
formula: 2 | ||
formula2: 6 | ||
cell: 10 | ||
formula: 20 | ||
formula2: 60 | ||
``` | ||
Note you can easily log streams yourself like so: | ||
```js | ||
s.map ( x => console.log('cell:', x) ) (cell) | ||
``` | ||
`s.log` just does this for you, and for multiple streams at once. | ||
#### merge | ||
`merge` is like `map` but allows you to respond to a list of streams changing. | ||
```js | ||
import { stream as s } from 'attain' | ||
const a = s.of() | ||
const b = s.of() | ||
const c = s.of() | ||
setTimeout(a, 1000, 'a') | ||
setTimeout(b, 2000, 'b') | ||
setTimeout(c, 3000, 'c') | ||
setTimeout(a, 4000, 'A') | ||
s.log({ combined: s.merge([a,b,c]) }) | ||
// after 3 seconds logs `combined: ['a', 'b', 'c']` | ||
// after 4 seconds logs `combined: ['A', 'b', 'c']` | ||
``` | ||
#### dropRepeats | ||
`dropRepeats` allows you to copy a stream and simultaneously remove duplicate values. | ||
```js | ||
import { stream as s } from 'attain' | ||
const a = s.of() | ||
const b = s.dropRepeats (a) | ||
s.log({ a, b }) | ||
a(1) | ||
a(1) | ||
a(2) | ||
a(2) | ||
a(3) | ||
``` | ||
Logs: | ||
``` | ||
a: 1 | ||
b: 1 | ||
a: 1 | ||
a: 2 | ||
b: 2 | ||
a: 2 | ||
a: 3 | ||
b: 3 | ||
``` | ||
#### interval | ||
`interval` creates a stream that emits the current time on an interval. | ||
```js | ||
import { stream as s } from 'attain' | ||
s.log({ now: s.interval(1000) }) | ||
``` | ||
Logs the time every 1000 ms: | ||
``` | ||
now: 1583292884807 | ||
now: 1583292885807 | ||
now: 1583292886807 | ||
``` | ||
#### afterSilence | ||
`afterSilence` copies a stream but it will ignore multiple values emitted within a duration that you set. | ||
```js | ||
import { stream as s } from 'attain' | ||
const a = s.of() | ||
const b = s.afterSilence (1000) (a) | ||
setTimeout(a, 0, 'first') | ||
setTimeout(a, 100, 'second') | ||
setTimeout(a, 500, 'third') | ||
setTimeout(a, 2000, 'fourth') | ||
setTimeout(a, 2500, 'fifth') | ||
setTimeout(a, 2501, 'sixth') | ||
s.log({ b }) | ||
``` | ||
Logs: | ||
``` | ||
b: 'third' | ||
b: 'fourth' | ||
b: 'sixth' | ||
``` | ||
#### scan | ||
`scan` allows you to access the previously emitted value of a stream, and then decide on the next value by transforming it in a reducer function. | ||
> 🤓 `scan` can be used to create something like Redux or the Elm Architecture | ||
```js | ||
import { stream as s } from 'attain' | ||
const action = s.of() | ||
const state = { | ||
count: 0 | ||
} | ||
const update = (state, action) => { | ||
if( action == 'INC') { | ||
return { count: state.count + 1 } | ||
} else if ('DEC') { | ||
return { count: state.count - 1 } | ||
} else { | ||
return state | ||
} | ||
} | ||
const model = s.scan (state) (update) (action) | ||
action('INC') | ||
action('INC') | ||
action('INC') | ||
action('DEC') | ||
s.log({ action, model }) | ||
``` | ||
Logs: | ||
``` | ||
model: { count: 0 } | ||
action: 'INC' | ||
model: { count: 1 } | ||
action: 'INC' | ||
model: { count: 2 } | ||
action: 'INC' | ||
model { count: 3 } | ||
action: 'DEC' | ||
model: { count: 2 } | ||
``` | ||
> ⚠ `attain` has other stream utilities but leaves them undocumented for now as a sign until they've been used more in production. Explore the source at your own peril 🦜 | ||
## Sum Types | ||
`attain` relies upon and provides a super powerful yet simple sum type API. | ||
#### What is a Sum Type? | ||
Sum Types are used to model when a single type has multiple shapes, and each shape has a semantic meaning. You've probably used Sum Types out in the wild without realising it. Here's a real world example: Promises. | ||
A promise can be in 3 states: `Pending` | `Resolved` | `Rejected`. | ||
A `Pending` Promise has no value, a `Rejected` Promise has a value, but that value is intended to represent an `Error` or failure. And a resolve promise has a value but is intended to represent a successful computation. | ||
We can describe sum types with the following syntax: | ||
```haskell | ||
data Promise = Pending | Resolved a | Rejected b | ||
``` | ||
The above means, there's a type of data called `Promise`, and it has 3 states, `Pending` which has no value. `Resolved` which has a value of type `a` and Rejected which has a value of type `b`. | ||
The types `a` and `b` are kind of like `<T>` and `<U>` in typescript. It just means, those types are allowed to be different but can be the same. | ||
Sometimes we call a sum type a _tagged union_ because the type of the data is a union of all the listed states, and each state is like the data was _tagged_ with a label. | ||
#### How do I create my own Sum Type? | ||
`attain` comes with a utility `tags` which is used to define new sum types: | ||
```js | ||
import * as A from 'attain' | ||
// data Promise = | ||
// Pending | Resolved a | Rejected b | ||
const Promise = | ||
A.type('Promise', ['Pending', 'Resolved', 'Rejected']) | ||
const pending = Promise.Pending() | ||
const resolved = Promise.Resolved('Hello') | ||
const rejected = Promise.Rejected(new Error('Oh no!')) | ||
Promise.isRejected( rejected ) | ||
// => true | ||
Promise.isResolved( rejected ) | ||
// => false | ||
``` | ||
#### How do I traverse all possible states? | ||
When you create a type, a lot of static functions are generated and attached to the type object. | ||
The most basic and most important is `fold`. It takes an object where each key must be exactly the tags specified in the definition of your type. Each value is a function that will receive the value of that specific tag. | ||
```js | ||
const Promise = | ||
A.type('Promise', ['Pending', 'Resolved', 'Rejected']) | ||
const log = | ||
Promise.fold({ | ||
Pending: () => null, | ||
Resolved: data => console.log('attain', data), | ||
Rejected: error => console.error('error', error) | ||
}) | ||
log( Promise.Pending() ) | ||
// null | ||
log( Promise.Resolved('Hello') ) | ||
// logs: data Hello | ||
log( Promise.Rejected('Oh No!') ) | ||
// logs: error Oh No! | ||
``` | ||
#### How do I interact with a particular state? | ||
If you want to transform the data inside a particular state you can use `map<TagName>`. So if you wanted to map over only the `Rejected` state you could use the `mapRejected` function. | ||
```js | ||
const Promise = | ||
A.type('Promise', ['Pending', 'Resolved', 'Rejected']) | ||
// Only logs for rejected promises | ||
const logFailure = | ||
Promise.mapRejected( err => console.error('Rejected', err) ) | ||
logFailure( Promise.Resolved() ) | ||
logFailure( Promise.Pending() ) | ||
logFailure( Promise.Rejected('Oh No') ) | ||
// logs: Rejected Oh No! | ||
``` | ||
If you just want to get the value out, you can use `get<TagName>Or`: | ||
```js | ||
const Promise = | ||
A.type('Promise', ['Pending', 'Resolved', 'Rejected']) | ||
// Only logs for rejected promises | ||
const gerError = | ||
Promise.getRejectedOr('No Error') | ||
getError( Promise.Resolved() ) | ||
// 'No Error' | ||
getError( Project.Rejected('Oh No') ) | ||
// 'Oh No' | ||
``` | ||
If you want to transform the value before extracting it, you can use `get<TagName>With`: | ||
```js | ||
const Promise = | ||
A.type('Promise', ['Pending', 'Resolved', 'Rejected']) | ||
// Only logs for rejected promises | ||
const getStack = | ||
Promise.getRejectedWith('', err => err.stack ) | ||
getStack( Promise.Resolved() ) | ||
// '' | ||
getError( Project.Rejected(new Error()) ) | ||
// 'Error at ./data/yourfile.js 65:43 @ ...' | ||
``` | ||
#### Either | ||
`attain` uses a sum type to represent success and failure: `Either` | ||
```haskell | ||
data Either = Y a | N b | ||
``` | ||
We use the tag `Y` to represent success ( Y for `Yes` ) and the tag `N` to represent failure ( N for `No` ). | ||
It's the same as any other manually defined sum type via `tags`, but it has a few extra powers because we can infer usage. | ||
E.g. `mapY` is aliased to `map`. Either is also a bifunctor, which means, it has two states that can be mapped over. | ||
Because we know that: `Either` has functions available like `Either.bifold` and `Either.bimap`. | ||
> 🤔 Extension to the base functionality of `A.type` is via `specs`. You can read about a standard way to extend functionality [here](https://gitlab.com/harth/stags/-/blob/master/docs/spec.md) | ||
You'll see Either pop up as you use `attain`, but just know, it's just another sum type with all the same functionality as our Promise examples. | ||
#### How do I serialize / deserialize my sum type? | ||
`attain`'s sum types were designed to be 100% serializable out of the box. Because we don't store methods on an instance, the sum types are just data, and there's no marshalling/unmarshalling to do! | ||
Each sum-type instance has a special toString which helps with debugging, but beyond that, it's just an object that looks like this: | ||
```js | ||
{ type: 'Promise', tag: 'Resolved', value: 'Hello' } | ||
``` | ||
Here's an example. | ||
```js | ||
const Promise = | ||
A.type('Promise', ['Pending', 'Resolved', 'Rejected']) | ||
Promise.isRejected(JSON.parse(JSON.stringify(rejected))) | ||
//=> true | ||
const { type, tag, value } = rejected | ||
Promise.isRejected( | ||
JSON.parse(JSON.stringify({ type, tag, value })) | ||
) | ||
//=> true | ||
``` | ||
This means you can store your sum type data in `LocalStorage`, `IndexedDB`, in your server somewhere! It's up to you. | ||
It's also such a simple data format that's it's easy to build custom utilities on top of it, but data has a bunch built in. | ||
## Z | ||
<img src="https://media.giphy.com/media/6KULP1HJan59S/giphy.gif"/> | ||
`Z` is the voltron, or captain planet, or power rangers... of `attain`. | ||
I recommend reading this a little later when you've internalised all the various core parts of `attain`. `Z` is a great example of the value of having an out of the box stream/lens/sum type library. | ||
When you start building apps using `attain`: `Z` is unbeatable for model persistence. | ||
So what is it? | ||
`Z` gives you a read/update/delete interface to an item in a stream of A. It then let's subscribe to changes/deletions. It's like queries you can subscribe to! | ||
```js | ||
import { $, Z }, * as A from 'attain' | ||
const todos = A.stream.of([ | ||
{ id: 1, title: 'Learn FP' } | ||
]) | ||
const $id = | ||
$ | ||
.$(R.map) | ||
.$filter( x => x.id == 5 ) | ||
const firstTodo = | ||
Z({ stream: todos }).$values.$filter( x => x.id == 5) | ||
firstTodo() | ||
//=> { id: 1, title: 'Learn FP' } | ||
A.stream.log({ | ||
'updated': firstTodo.$stream, | ||
// ignores duplicates and waits 1s to emit | ||
'throttled': firstTodo.$throttled(1000), | ||
'removed': firstTodo.$removed | ||
}) | ||
firstTodo.title( x => x.toUpperCase() ) | ||
// logs: updated: { id: 1, title: 'LEARN FP' } | ||
// logs: throttled: { id: 1, title: 'LEARN FP' } | ||
todos() | ||
//=> stream(Y([{ id: 1, title: 'LEARN FP' }])) | ||
firstTodo.title( x => x+'!' ) | ||
firstTodo.title( x => x+'!' ) | ||
firstTodo.title( x => x+'!' ) | ||
// logs: updated: { id: 1, title: 'LEARN FP!' } | ||
// logs: updated: { id: 1, title: 'LEARN FP!!' } | ||
// logs: updated: { id: 1, title: 'LEARN FP!!!' } | ||
// logs: throttled: { id: 1, title: 'LEARN FP!!!' } | ||
firstTodo.remove() | ||
// logs: removed: { id: 1, title: 'LEARN FP' } | ||
todos() | ||
// => [] | ||
``` | ||
You can take advantage of the `.throttled` and `.deleted` streams for making server side changes. | ||
```js | ||
// Be notified of writes ignoring duplicates | ||
// and debouncing a specified amount of time | ||
firstTodo.$throttled.map( | ||
// Yes! you can _safely_ use sql client side. | ||
// check out https://github.com/hashql/hashql | ||
({ id, title }) => sql` | ||
update todos | ||
set title = ${title} | ||
where id = ${id} | ||
` | ||
) | ||
// Be notified when your result is deleted from the list. | ||
firstTodo.$removed.map( | ||
({ id, title }) => sql` | ||
delete from todos where id = ${id} | ||
` | ||
) | ||
``` | ||
Acknowledgements | ||
---------------- | ||
<img style="max-width: 200px;" src="https://media.giphy.com/media/rIq6ASPIqo2k0/giphy.gif"/> | ||
Thank you to the mithril community for adopting streams and providing a readily available MIT implementation. | ||
(More to come this project!) | ||
This package has been renamed [how](https://www.npmjs.com/package/how) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
775134
4