Security News
RubyGems.org Adds New Maintainer Role
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
This libraries optimizes for composition and safety without forgetting that this is ultimately still Javascript.
npm install attain
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!'
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!')
You'll see the usage of some utilities in the example code. Here's a quick breakdown.
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
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.
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
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.
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
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
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
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
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
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
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
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 🦜
attain
relies upon and provides a super powerful yet simple sum type API.
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.
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
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!
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 @ ...'
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 viaspecs
. 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.
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
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}
`
)
Thank you to the mithril community for adopting streams and providing a readily available MIT implementation.
(More to come this project!)
FAQs
A library for modelling and accessing data.
The npm package attain receives a total of 3 weekly downloads. As such, attain popularity was classified as not popular.
We found that attain demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
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.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
Security News
Node.js will be enforcing stricter semver-major PR policies a month before major releases to enhance stability and ensure reliable release candidates.
Security News
Research
Socket's threat research team has detected five malicious npm packages targeting Roblox developers, deploying malware to steal credentials and personal data.