I'm a functional programming beginner. I'm working on a React Native app using Ramda. The app lets users maintain their houses.
I have written function called asyncPipe
which lets me pipe promises and normal functions. I use it for the loginFlow
which currently has a http request (getHouseList
) as its last function.
const asyncPipe = (...fns) => x => fns.reduce(async (y, f) => f(await y), x);
const loginFlow = asyncPipe(
// ... someFunctions
getHouseList
);
// used later like this in LoginForm.js's handleSubmit():
const list = await loginFlow(credentials);
So, after logging in, the app loads the user's houses. Now depending on whether he has only one or multiple houses I would like to send the user either to list view to choose a house or a detail view if he only has one house. Additionally, I would like to dispatch a Redux action to save the list in my reducer and another action to pick the house if there is only one.
Currently I do it like this:
const list = await loginFlow(credentials);
dispatch(addHouses(list));
if (list.length > 1) {
navigate('ListScreen')
} else {
dispatch(pickHouse(list[0]);
navigate('DetailScreen') ;
}
But as you can see that is super imperative. It seems like I have to 'fork' the list and use it twice in the pipe (because Redux' dispatch
does not have a return value).
My main question is:
How to do this more functional / declaratively (if there is a way)?
A little sub question I have would be, whether its okay to be imperative here / if doing it functional is a good idea.
You could probably extend your async pipeline, using something like tap
:
const loginFlow = asyncPipe(
// ... some functions
getHouseList,
tap(compose(dispatch, addHouses)),
tap(unless(list => list.length > 1, list => dispatch(pickHouse(list[0])))),
list => navigate(list.length > 1 ? 'ListScreen' : 'DetailScreen', list)
);
Whether this is worth doing will depend upon your application. If the pipeline is already a longish one, then it would probably be cleaner to add things to the end this way, even if they're not particularly functional sections. But for a short pipeline, this might not make much sense.
You also might want to look at the now-deprecated, pipeP or its replacement, pipeWith
(then
).
But you asked in the title about forking a parameter. Ramda's converge
does exactly that:
converge(f, [g, h])(x) //=> f(g(x), h(x))
This allows you to pass more than two functions as well, and to pass more than one parameter to the resulting function:
converge(f, [g, h, i])(x, y) //=> f(g(x, y), h(x, y), i(x, y))
Given that we can use R.then
and R.otherwise
, then an asyncPipe
is not really needed. One of the principle of functional programming is actually delegating orchestration...
Finally, of course you can be more declarative, and a good way to start is trying to avoid imperative control flows. R.ifElse
will definitely help you here :)
If your code has side effects,
then use R.tap
in your pipes :)
const fake = cb => () => cb([
{ name: 'Hitmands', id: 1 },
{ name: 'Giuseppe', id: 2 },
]);
const fakeApiCall = () => new Promise(resolve => setTimeout(fake(resolve), 500));
const dispatch = action => data => console.log(`dispatch("${action}")`, data);
const navigate = view => data => console.log(`navigate("${view}")`, data);
const loginFlow = (...fns) => R.pipe(
R.tap(() => console.log('login Flow Start')),
fakeApiCall,
R.then(R.pipe(
...fns,
R.tap(() => console.log('login Flow End')),
)),
)
const flow = loginFlow(
R.tap(dispatch('addHouse')), // use tap for side effects
R.ifElse(
R.pipe(R.length, R.gt(R.__, 1)), // result.length > 1
R.tap(navigate('ListScreen')), // onTrue
R.pipe( // onFalse
R.tap(dispatch('pickHouse')),
R.tap(navigate('DetailScreen')),
),
),
);
/* await */ flow();
/** UPDATES **/
const isXGreaterThan1 = R.gt(R.__, 1);
const isListLengthGreatherThanOne = R.pipe(R.length, isXGreaterThan1);
console.log(`is list.length > 1`, isListLengthGreatherThanOne([1, 2, 3]));
console.log(`is list.length > 1`, isListLengthGreatherThanOne([1]));
console.log(`is list.length > 1`, isListLengthGreatherThanOne([]));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>