Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fantasy land update #166

Merged
merged 15 commits into from
Feb 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 108 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ __Other features__

* Supports the transducer protocol. You can for instance transduce streams with
[Ramda](http://ramdajs.com/) and [transducers.js](https://github.com/jlongster/transducers.js).
* Complies to the [fantasy land](https://github.com/fantasyland/fantasy-land)
applicative specification.
* [Elegant support for promises](#using-promises-for-asynchronous-operations).
* [Atomic updates](#atomic-updates).

## Examples
Expand Down Expand Up @@ -209,19 +206,31 @@ after an actual response has been received (otherwise `responses()` would return
streams has received a value (this behaviour can be circumvented with
[flyd.immediate](#flydimmediatestream)).

### Using promises for asynchronous operations
### Promises
Flyd has two helpers for dealing with promises: `flyd.fromPromise` and `flyd.flattenPromise`.

Flyd has inbuilt support for promises. Similarly to how a promise can never be
resolved with a promise, a promise can never flow down a stream. Instead the
fulfilled value of the promise will be sent down the stream.
Let's say you're building a filtered list. It is important to you that the latest filter always corresponds
to the latest promise and its resolution. using `flyd.fromPromise` guarantees the ordering, and can skip intermediate results.

```javascript
var urls = flyd.stream('/something.json');
var responses = flyd.stream(requestPromise(urls()));
flyd.on(function(responses) {
console.log('Received response!');
console.log(responses());
}, responses);
```js
const filter = flyd.stream('');
const results = filter
.pipe(flyd.chain(
filter => flyd.fromPromise(requestPromise(`https://example.com?q=${filter}`))
));
```

On the other hand let's say you want to sum some numbers from a service you've written.
Every time someone clicks on your site you want to send a request and get back a random number to be tallied.

`flyd.flattenPromise` gives you the guarantee that every promise resolution will be handled, regardless of order.

```js
const clicks = flyd.stream();
const total = clicks
.map(getNumberAsync)
.pipe(flyd.flattenPromise)
.pipe(flyd.scan((acc, v)=> acc + v, 0));
```

### Mapping over a stream
Expand Down Expand Up @@ -426,6 +435,60 @@ __Example__
var numbers = flyd.stream(0);
var squaredNumbers = flyd.map(function(n) { return n*n; }, numbers);
```
### flyd.chain(fn, s)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the FL-standard s.chain(fn) or the static chain(fn)(s)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about them? can you elaborate?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether I can use them instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have never seen the syntax like here in this context. Any motivation for it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's actually no FL-standard s.chain(fn).

If you read the spec, there's a requirement for a prefixed fantasy-land/chain to comply with the spec.

flyd.chain can be used in a number of ways:

  1. directly
flyd.chain(fn, s);
  1. through pipe
s.pipe(flyd.chain(fn));
  1. bound to the stream as fantasy-land/chain
s['fantasy-land/chain'](fn);

All of these ways are functionally equivalent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean here:
https://github.com/fantasyland/fantasy-land#chain-method

m.chain(f)

As a monad, flyd.stream object has to implement the .chain method.

I didn't mean the prefixed methods.
They are in addition, more for the library creators and the situation
when you have low level libraries used by higher level ones.
However, there is no higher level library for flyd.

I can see the other ways, but none of them is really as usable,
they are more for a low level library.

So I would need to write a micro-wrapper of my own,
which I shall do if the direct methods won't be supported out of the box,
but I've thought it might be useful for more people than just me.

Copy link
Collaborator Author

@nordfjord nordfjord Jan 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the part of the spec I linked says:

Further in this document unprefixed names are used just to reduce noise.

Meaning it's the prefixed methods that matter. Unprefixed methods are not part of the spec and as such can for the most part be treated as having undefined behaviour.

Now we really have 2 camps when it comes to FP in JS, there's the Haskell camp, and the Scala camp.

The Scala camp prefers method chaining and the Haskell camp prefers function composition.

To me, the pipe method should make both camps happy.

The function composition camp can write:

import {stream} from 'flyd';
import {chain, map, compose, prop} from 'ramda';

const filter$ = stream('');
const result$ = compose(
  map(prop('content')),
  chain(compose(fromPromise, getResult))
)(filter$);

and the method chaining camp can write:

import {stream} from 'flyd';
import {chain, map, prop} from 'ramda';

const filter$ = stream('');
const result$ = filter$
  .pipe(chain(compose(fromPromise, getResult)))
  .pipe(map(prop('content')));

Note: chain and map could also be imported from flyd, but I wanted to show that other libraries supporting the fantasy-land spec can be used too.

`fn` must return a stream.

`fn` is run every time a value is pushed into `s`.
Returns a single stream of merged values from the created streams.

Ends when every created stream and the main stream ends

__Signature__

`(a -> Stream b) -> Stream a -> Stream b`

__Example__
```javascript
var filter = flyd.stream('filter');
var search_results = flyd.chain(function(filter){
return flyd.stream(getResults(filter));
}, filter);
```

### flyd.ap(valueStream, functionStream)

Applies the value in `valueStream` to the function in `functionStream`

__Signature__
`Stream a -> Stream (a -> b) -> Stream b`

__Example__
```javascript
function add3(x) { return x + 3; }
flyd.ap(flyd.stream(5), flyd.stream(add3)) // stream(8);
```

while it can not seem useful immediately consider this example

```javascript
var get_results = function (filter, sortProperty, sortDirection) {
return flyd.stream(fetch(`${base_url}/search?q=${filter}&sort=${sortProperty} ${sortDirection}`))
};

// this would eventually be linked to an input field
var filter = flyd.stream('');
var sortProperty = flyd.stream('name');
var sortDirection = flyd.stream('descending');

var results = flyd.stream(flyd.curryN(3, get_results))
.pipe(flyd.ap(filter))
.pipe(flyd.ap(sortProperty))
.pipe(flyd.ap(sortDirection))
.pipe(flyd.map(function(d){ return d; }));
```

In the above example you have a stream of results that triggers a call for get_results
every time `filter`, `sortProperty`, or `sortDirection` is changed.

### flyd.on(fn, s)

Expand Down Expand Up @@ -555,7 +618,36 @@ names(); // 'Bohr'
A stream that emits `true` when the stream ends. If `true` is pushed down the
stream the parent stream ends.

### stream.map(f)
### stream.pipe(fn)

Returns the result of applying function `fn` to the stream.

__Signature__
Called bound to `Stream a`: `(Stream a -> Stream b) -> Stream b`

__Example__
```javascript
// map a stream
var numbers = flyd.stream(0);
var squaredNumbers = numbers
.pipe(flyd.map(function(n) { return n*n; }));

// Chain a stream
var filter = flyd.stream('filter');
var search_results = filter
.pipe(flyd.chain(function(filter){
return flyd.stream(getResults(filter));
}));

// use with a flyd module
var filter = require('flyd/module/filter');
var numbers = flyd.stream(0);
var isEven = function(x){ return x % 2 === 0; };
var evenNumbers = numbers
.pipe(filter(isEven));
```

### stream.map(f) __Deprecated__

Returns a new stream identical to the original except every
value will be passed through `f`.
Expand All @@ -574,7 +666,7 @@ var numbers = flyd.stream(0);
var squaredNumbers = numbers.map(function(n) { return n*n; });
```

### stream1.ap(stream2)
### stream1.ap(stream2) __Deprecated__

`stream1` must be a stream of functions.

Expand Down Expand Up @@ -624,7 +716,6 @@ Modules listed with names in the format `flyd/module/filter` are builtin to the
| --- | --- |
| [flyd/module/**filter**](module/filter) | Filter values from stream based on predicate. |
| [flyd/module/**lift**](module/lift) | Maps a function taking _n_ parameters over _n_ streams. |
| [flyd/module/**flatmap**](module/flatmap) | Maps a function over a stream of streams and flattens the result to a single stream. |
| [flyd/module/**switchlatest**](module/switchlatest) | Flattens a stream of streams. The result stream reflects changes from the last stream only. |
| [flyd/module/**keepwhen**](module/keepwhen) | Keep values from one stream only when another stream is true. |
| [flyd/module/**obj**](module/obj) | Functions for working with stream in objects. |
Expand Down
Loading