Skip to content

Instantly share code, notes, and snippets.

@rupeshdabbir
Created November 5, 2020 18:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rupeshdabbir/cea627ea18ff71045dac430177456136 to your computer and use it in GitHub Desktop.
Save rupeshdabbir/cea627ea18ff71045dac430177456136 to your computer and use it in GitHub Desktop.
functional context

Problem

The current approach to functional programming only takes into consideration one piece of data going through the pipeline. This approach makes a lot of sense when you have a very linear flow, where we mutate the data along the way. The code doesn't hold well when we need to create another piece of data and keep the original data to use downstream.

A simple example would be:

const consolidateFilters = (fieldSet) => {
  const value = concatFiltersAndParameters(fieldSet)
  return flow([set('filters', value), omit('parameters')])(fieldSet)
}

Here we need to use the fieldSet to create a values object, at the same time we need to keep fieldSet in the scope to use on the next line of code. There is no easy way to express this flow using the current functional programming operators we have at lodash/fp.

We need to create a top level abstraction to solve this problem.

Solving parallel flow using context

A possible solution to this problem is using a top level abstraction that we can use to keep fieldSet and any other data derived from fieldSet to be used downstream in the flow. We call this abstraction context.

The context will be the main object used through the data flow, and it keep inside it the original data passed to the function as well any derived data created by the dataflow to be used downstream.

Let's take a look on what we want to achieve on the previous example:

   fieldSet
      |
      |+------------+
      |             |
      |           values
      |             |
fieldSet.filters <--+
      |
      |
Remove paramaters
      |
      |
Return fieldSet

On this flow we:

  • Receive fieldSet
  • Keep fieldSet at the side
  • Calculate values using fieldSet
  • Assign values to fieldSet.filters
  • Remove parameters from fieldSet
  • Return fieldSet

Let's take a look at how we can solve this problem using context:

const consolidateFilters = flow([
  createContextWith('fieldSet'),
  setToContext('values', flow([get('fieldSet'), concatFiltersAndParameters])),
  setToContext('fieldSet.filters', get('values')),
  omit('fieldSet.parameters'),
  get('fieldSet'),
])

Using context, we can:

  • fork the flow, calculate a derived data, and come back to the main data flow again
  • Use the previous derived data on any step downstream, i.e. setToContext('fieldSet.filters', get('values')),

To enable this flow, we need to define two functions:

// Create the initial context object and assign the data to it under the fieldName
const createContextWith = (fieldName: string) => (obj: any) =>
  set(fieldName, obj)({})

// Make possible to use the context to fork the main data flow, create a new piece of data
// and inject it back to the context to be used on the main data flow downstream
const setToContext = (fieldPath: string, fn: (...args: any[]) => any) => (
  cxt: any
) => set(fieldPath, fn(cxt))(cxt)

Using this solution, we can write the functional code capable of creating derived data linearly.

Credits: Pablo Lacerda de Miranda

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment