Socket
Socket
Sign inDemoInstall

attain

Package Overview
Dependencies
4
Maintainers
1
Versions
172
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    attain

A library for modelling and accessing data.


Version published
Weekly downloads
244
increased by41.04%
Maintainers
1
Install size
3.26 MB
Created
Weekly downloads
ย 

Readme

Source

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

Installation

npm install attain

Quick Start

Queries:

import { $ } from 'attain'

const d = $.a.b.c.d

let state = {}
state = d( 2 ) (state)

state 
// => { a: { b: { c: { d: 2} } } }

Sum Types:

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
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

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.

run(
    2
    , x => x * 2
)
// => 4
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

Queries

What are queries? Well to be honest, they are a thing I made up.

But... they are basically the same thing as lenses 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!

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.

$

$ lets you get/set and transform a value that you've queries.

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

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.

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

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:

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.

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.

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.

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.

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

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:

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:

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.


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.

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:

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:

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

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

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:

{ type: 'Promise', tag: 'Resolved', value: 'Hello' }

Here's an example.

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

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!

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.

// 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

Thank you to the mithril community for adopting streams and providing a readily available MIT implementation.

(More to come this project!)

Keywords

FAQs

Last updated on 20 Apr 2020

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

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

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with โšก๏ธ by Socket Inc