Meiosis Documentation

< Previous | Next > | Table of Contents

05 - Meiosis with Mergerino

In the previous section, we set up the Meiosis pattern with an update stream of function patches.

In this section, we will use another approach - my personal favourite - using a library called Mergerino. The Meiosis pattern is flexible enough that you can use either of these approaches or even one of your own.

Introducing Mergerino

Mergerino is a brilliant utility that Daniel Loomer wrote in less than 30 lines of code. We will use patches on our update stream that Mergerino can use to produce the updated state from our accumulator function.

Let's say we have this initial state:

const initial = {
  temperature: {
    value: 22,
    units: 'C'
  }
};

Imagine that our patches are objects that describe how we want to update the state. If we want to change the temperature value to 23, we would call:

update({ value: 23 });

To change the units:

update({ units: 'F' });

To convert the value at the same time as changing the units:

update({ value: 72, units: 'F' });

How do we write an accumulator function that handles these object patches to update the state?

Mergerino exports a function, that we call merge, which takes a target object as its first parameter, and patch objects in the remainder of the parameters. It patches the target object by copying over the properties from the patch objects onto the target object:

merge({ value: 22, units: 'C' }, { value: 23 });
// result:
{ value: 23, units: 'C' }

merge({ value: 23, units: 'C' }, { comfortable: true })
// result:
{ value: 23, units: 'C', comfortable: true }

If you find that this looks like Object.assign, you are correct: in these examples, merge does the equivalent. However, merge has capabilities beyond what you can do with Object.assign.

Patching based on the current value

Within a patch, you can use the current value of the target object to determine the updated value. Just pass a function as the value of the property. Mergerino passes the current value of that property to the function, and assigns the function's return value back to that property.

This makes it easy to update a value using the current value. For example, say that we want to increment the temperature value by 1. We need the current value to compute the updated value. We can use a function for value:

merge({ value: 22, units: 'C' }, { value: x => x + 1 }); // The function receives 22
// result:
{ value: 23, units: 'C' }

By passing a function for the value property, Mergerino passes the current value of that property to the function. Our function receives 22, adds 1 and returns 23, which Mergerino assigns back to the value property.

Deep Patching

Object.assign performs a shallow merge. If our target object is:

{ air:   { value: 22, units: 'C' },
  water: { value: 84, units: 'F' }
}

And we want to change the air value to 25 by calling:

Object.assign(
  { air:   { value: 22, units: 'C' },
    water: { value: 84, units: 'F' }
  },
  { air:   { value: 25 } }
);

We get this result:

{ air:   { value: 25 }, // we lost the units!
  water: { value: 84, units: 'F' }
}

We lost the units! This is because properties are merged only at the first level. Beyond that, values are completely replaced instead of merged. So { value: 22, units: 'C' } got replaced with { value: 25 }.

With Mergerino, we can merge properties deeper than the first level. Because merging happens at every level, we can update the value without losing the units:

merge(
  { air:   { value: 22, units: 'C' },
    water: { value: 84, units: 'F' }
  },
  { air: { value: 25 } }
);
// result:
{ air:   { value: 25, units: 'C' }, // now we didn't lose the units!
  water: { value: 84, units: 'F' }
}

Deep patching and function patching can also be used together:

merge(
  { air:   { value: 22, units: 'C' },
    water: { value: 84, units: 'F' }
  },
  { air:  { value: x => x + 8 } }
);
// result:
{ air:   { value: 30, units: 'C' }, // increased the value by 8, didn't lose the units
  water: { value: 84, units: 'F' }
}

If we want to avoid deep patching and instead want to replace a property, we can use a function. Say we want to set air to { replaced: true } without keeping value and units:

merge(
  { air:   { value: 22, units: 'C' },
    water: { value: 84, units: 'F' }
  },
  { air:   () => ({ replaced: true }) } // use a function to replace the value
);
// result:
{ air:   { replaced: true },
  water: { value: 84, units: 'F' }
}

We can also use a function at the top level to produce a completely new result without keeping any of the previous values. This could be useful, for example, to re-initialize application state.

merge(
  { ... }, // doesn't matter what the previous state was
  () => (
    { air:   { value: 22, units: 'C' },
      water: { value: 84, units: 'F' }
    }
  )
);
// result:
{ air:   { value: 22, units: 'C' },
  water: { value: 84, units: 'F' }
}

Deleting a property

Finally, we can use undefined as a property value when we wish to delete that property:

merge(
  { air:   { value: 22, units: 'C' },
    water: { value: 84, units: 'F' }
  },
  { air: undefined }
);
// result:
{ water: { value: 84, units: 'F' } }

merge(
  { air:   { value: 22, units: 'C' },
    water: { value: 84, units: 'F' }
  },
  { air: { value: undefined } }
)
// result:
{ air:   { units: 'C' },
  water: { value: 84, units: 'F' }
}

Try it out. Using the code window below, try the following exercises. Use console.log to verify your answers.

Exercises

  1. Change water to { value: 84, units: 'F' }
  2. Toggle the comfortable property with a function that changes the value to the opposite of what it was
  3. Change the air value to 20 without losing the units
  4. Delete the invalid property.

Solution

Show solution

Using Mergerino with Meiosis

To use Mergerino with Meiosis, we can pass object patches onto the update stream and use them in the accumulator to update the state.

For example, to increment the temperature value:

increment: (update, amount) => {
  update({
    temperature: {
      value: (x) => x + amount
    }
  });
};

Now we need to use these object patches in the accumulator function. Remember that the accumulator gets the current state and the incoming patch as parameters, and must return the updated state. We can use merge:

const states = flyd.scan(
  (state, patch) => merge(state, patch),
  initial,
  update
);

Notice that the accumulator function that we are passing is:

(state, patch) => merge(state, patch);

We have a function that takes (state, patch) and calls merge with (state, patch). But, that is the same as merge itself! So, we can use it directly:

const states = flyd.scan(merge, initial, update);

Putting it all together, we have:

Exercises

Try it out: notice that the initial state appears in the output on the right. Within the console, type and then press Enter:

actions.increment(update, 2)

actions.changeUnits(update)

In the output on the right, you'll see the updated states.

When you are ready, continue on to the next section, where we will combine the state and update into cells.

< Previous | Next > | Table of Contents


Meiosis is developed by foxdonut (Twitter / GitHub) and is released under the MIT license.