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.
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
usingfieldSet
- Assign
values
tofieldSet.filters
- Remove
parameters
fromfieldSet
- 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.