Concepts: Semigroup

1. Motivation

Unlike the previous sections, which were all about transforming consumers, this section is explicitly about combining consumers. It provides an analogue to normal function composition.

The motivating example for this section can be seen as breaking up a consumer into smaller parts then recombining them with no processing inbetween the individual parts. I have two lists of todo items, finished and unfinished. When a todo item is marked as finished it must go in the finished list and be removed from the unfinished list.

{semigroup-example 1}
const todos = (state, action) => {
  if (action.type === 'FINISH') {
    return {
      ...state,
      finished: state.concat(action.todo)
      unfinished: filter(
        (todo) => todo === action.todo,
        state
      )
    };
  } else {
    return state;
  }
};

While small the example does serve to highlight that two concerns are mixed when handling the FINISH action. That of removing the todo from the unfinished list and adding it to the finished list. Using the Semigroup interface I can break this into two reducers, one responsible solely for finished todos and the other responsible for unfinished todos.

{semigroup-example-2 1}
const finishedTodos = (state, action) => {
  if (action.type === 'FINISH') {
    return {
      ...state,
      finished: state.concat(action.todo)
    };
  } else {
    return state;
  }
}

const unfinishedTodos = (state, action) => {
  if (action.type === 'FINISH') {
    return {
      ...state,
      unfinished: filter(
        (todo) => todo === action.todo,
        state
      )
    };
  } else {
    return state;
  }
}

Then they can simply be combined

{semigroup-example-2 1} +=
Semigroup.concat(finishedTodos, unfinishedTodos);

2. Details

The Semigroup interface contains a concat method, which takes two arguments, and a concatAll method which is variadic to reduce the amount of nested concat calls that are necessary. The concatAll method requires all input and output state to be of the same type.

{types/Semigroup.js 2}
export type Semigroup<Static, In, OutA, OutB> = {
  concat: ((In, Static) => OutA, (OutA, Static) => OutB) => (In, Static) => OutB,
  concatAll: (...Array<(In, Static) => In>) => (In, Static) => In
};

The concat method takes two consumers and returns a new one. It passes the same Static value to each input consumer, and passes the output of the first consumer as the input to the second consumer. It must obey the law of associativity so that different groups of applications of concat do not change the final value.

{semigroup-laws-associativity 2}
concat(concat(c, c'), c'') === concat(c, concat(c', c''))

With the laws and semantics defined above, the implementation of concat is straightforward.

{semigroup-concat-implementation 2}
concat: (c, c_) => (
  (s, a) => (
    c_(c(s, a), a)
  )
)

The concatAll implementation is defined in terms of concat and is simply a reduction over the input arguments.

{semigroup-concatAll-implementation 2}
concatAll: (...cs) => (
  cs.reduce(SemigroupI.concat, Monoid.identity)
)

Finally it is all wrapped up in a module.

{Semigroup.js 2}
{import-types-semigroup, 3}
{import-monoid, 8:3}

const SemigroupI : Semigroup<*, *, *, *> = {
  {semigroup-concat-implementation, 2}
  ,
  {semigroup-concatAll-implementation, 2}
};

export default SemigroupI

3. Import

The Semigroup type needs to be imported.

{import-types-semigroup 3}
import type { Semigroup } from './types/Semigroup';

Used in section 2

Other modules need to import Semigroup.

{import-semigroup 3}
import Semigroup from './Semigroup';

Used in section 12:1


Previous ChapterNext Chapter