Meiosis Documentation

Table of Contents

Services

James Forbes shared his idea of Services. In this section, we'll look at James' version using streams, and another version using a separate computed state function and a service trigger. For the latter, we'll use two variants, one with Barney Carroll's Patchinko, and one with function patches.

James explains that while one-off actions occur after click events, user input, and so on, services are for ongoing state synchronization. They can produce computed properties, store and retrieve state from local storage, and trigger other actions.

Using Streams

James' version uses streams to implement services. The structure is as follows:

{
  initial: state => initialState,
  start: states => patches
}

A service has an initial function which produces the service's initial state. The start function takes the Meiosis stream of states and returns a stream of patches. The service emits patches onto this stream.

The application's initial state is combined with each service's initial state to produce the final initial state:

const services = [ /* ... */ ];

const initialState = () => {
  const state =
    { boxes: []
      , colors:
      [ "red"
        , "purple"
        , "blue"
      ]
    };
  return Object.assign({},
    state,
    services
      .map(s => s.initial(state))
      .reduce(R.merge, {})
  );
};

Then, every service is started by passing in the stream of states, and mapping the resulting stream of patches onto the update stream:

services.map(s => s.start(states).map(update));

When a service emits a patch onto its stream, it is passed on to the update stream.

Colored Boxes Example

James shared an example where you have colored boxes that you can click on to add them to a list. The boxes are displayed one next to the other, with a description of how many boxes of each color are in the list. You can remove a box from the list by clicking on it.

In the example, there are three services:

Each service has an initial and start function. For example, the StatsService initializes its state with 0 for every box color, and computes the number of instances of each color:

const StatsService = {
  initial(state) {
    return state.colors
      .map(R.objOf)
      .map(K(0))
      .reduce(R.merge, {});
  },
  start(state) {
    return dropRepeats( state.map( x => x.boxes ) )
      .map( R.countBy(I) )
      .map( R.assoc("stats") );
  }
};

Notice the call to dropRepeats. This is necessary because the stream of patches produced by the service is fed back into the Meiosis update stream. This in turn produces an updated state, which triggers the service again. To avoid an infinite loop, dropRepeats does not emit a value when it is the same as the previous one:

function dropRepeats(s) {
  var ready = false;
  var d = m.stream();
  s.map(function (v) {
    if (!ready || v !== d()) {
      ready = true;
      d(v);
    }
  });
  return d;
}

The example uses function patches. Here is the setup for the Meiosis pattern:

const update = m.stream();
const T = (x, f) => f(x);
const state = m.stream.scan( T, initialState(), update );
const element = document.getElementById("app");
states.map(view(update)).map(v => m.render(element, v));

The complete example is below.

Flexibility

Using streams gives you the flexibility of being able to hook into them and wiring them as you wish.

Using Computed State and Services

An alternative to emitting patches from services is to define computed state as a function that receives the current state as a parameter and returns a patch. Then, these computed functions can be combined together with reduce to produce a single computed function that receives the current state and produces an updated state containing the computed properties.

Computed state runs synchronously. For asynchronous changes, we define a service function that receives the current state and the update stream, and decides whether to call update(). Our service structure is thus:

{
  initial: state => initialState,
  computed: state => patch,
  service: (state, update) => { /* call update() based on state */ }
}

With Patchinko

In this section, we'll use Patchinko, which we looked at in the tutorial.

To use Patchinko, we emit patches as objects instead of functions, and we use P as our accumulator:

const states = m.stream.scan( P, initialState(), update );

Instead of a start function, we'll use a computed function to which we'll pass the latest state from the Meiosis states stream. The computed function returns a patch:

{
  computed: state => patch
}

Before, we took a stream of states and we returned a stream of patches; now, we just take a state and return a patch.

We'll assemble the computed functions into an array:

const computes = [ f1, f2, f3 ];

Each function f takes the state and returns a patch to update the state. Thus calling f(state) gives us a patch. To apply the patch, we call P(state, f(state)). Finally, to combine the array of functions into a single function, we can use reduce:

// Top-level computed function
const computed = state =>
  computes.reduce((x, f) => P(x, f(x)), state);

This gives us a single top-level computed function that takes the state, calls all computed functions, and produces the updated state. We can just map this computed function to our stream of states:

const states = m.stream.scan( P, initialState(), update )
  .map(computed);

Computed functions are for synchronous calculations based on the state. For asynchronous changes, such as loading data from a server, we'll separately define services as functions that receive the current state and the update stream, and call update as they see fit:

service: (state, update) => {
  // determine whether to call update() based on state
}

As in the previous section, we have to be careful about infinite loops. Indeed, when the service calls update(), the service will be triggered again.

In this example, the LocalStorageService doesn't call update, so it's not an issue:

service(state, _update) {
  T(
    state,
    R.pipe(
      R.pick(["boxes"]),
      x => localStorage.setItem("v1", JSON.stringify(x))
    )
  );
}

You can see another example of services in the Next-Action-Predicate section of the SAM Pattern. There, the function has a condition which ensures not to keep calling update() in an infinite loop.

After assembling service functions into an array, wiring them up is simply a matter of calling them every time the state changes:

states.map(state =>
  services.forEach(service => service(state, update)));

Finally, as before we use our states stream to render the view:

states.map(view(update)).map(v => m.render(element, v));

You will find the complete example below.

With Function Patches

We can also use this approach with function patches instead of Patchinko. Remember that with function patches, we produce functions f(state) => updatedState instead of object patches, and we wire up Meiosis like this:

const T = (x, f) => f(x);
const update = m.stream();
const states = m.stream.scan( T, initialState(), update );

Our services have the same structure as before, namely:

{
  initial: state => initialState,
  computed: state => patch,
  service: (state, update) => { /* call update() based on state */ }
}

The only difference is that patch is now a function instead of an object.

Again we have an array of functions for computed state:

const computes = [ f1, f2, f3 ];

But now each function f takes the state and returns a function patch to update the model. When we call f(state), we get a function. To apply the patch, we just call the function: f(state)(state). Finally, we use reduce to write our top-level service function:

// Top-level computed function
const computed = state =>
  computes.reduce((x, f) => f(x)(x), state);

As before, we map our service function to the states stream, and use the states stream to render the view:

const states = m.stream.scan( T, initialState(), update )
  .map(service);
states.map(view(update)).map(v => m.render(element, v));

Have a look at the complete example below.

Conclusion

We can wire up services in different ways, and use them for computed properties, state synchronization, and other purposes. Please note, however, that not everything belongs in a service, so it's important to avoid getting carried away.

Table of Contents


Meiosis is developed by @foxdonut00 / foxdonut and is released under the MIT license.