Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

attain

Package Overview
Dependencies
Maintainers
1
Versions
172
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

attain - npm Package Compare versions

Comparing version 0.0.191 to 0.0.192

2

package.json
{
"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",

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)
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc