4

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.

J. Hesters
  • 13,117
  • 31
  • 133
  • 249
  • 3
    Of course it's okay to be imperative, if it gets the job done. There's no reason to do FP gymnastics only for the sake of doing FP gymnastics if you ask me. – AKX Feb 20 '19 at 13:04
  • 1
    Given that `dispatch` has no return value, there's nothing functional about it anyway that would allow you to use a pipe. – Bergi Feb 20 '19 at 13:23
  • @Bergi Yup. I had the idea of writing a wrapper like: `const myDispatch => actionCreator => payload => { dispatch(actionCreator(payload)); return payload; }`, but I don't know if thats a good idea. – J. Hesters Feb 20 '19 at 13:30
  • A few times I've had to do some side processing while doing a chain and the only semi-universal thing I've really found is to basically use `map` to do `.map(x => { doSomeProcessing(x); return x })`. Same in Java, actually when I have a stream and need to do do two things with each value (`.peek` is discouraged, although it does what I need). I'm interested what the "proper" way to do this functionally is. – VLAZ Feb 20 '19 at 13:40
  • 2
    Perhaps you could wrap `dispatch` so that it returns its argument. (Or perhaps using [`tap`](https://ramdajs.com/docs/#tap) would do the job just as well.) Then you may be able to compose promise-returning functions with [`then`](https://ramdajs.com/docs/#then). – customcommander Feb 20 '19 at 14:10
  • @customcommander `tap` looks awesome. I'm gonna see, if I can write a custom middleware for it #challengeAccepted – J. Hesters Feb 20 '19 at 20:05
  • I think you will find [this Q&A](https://stackoverflow.com/a/46918344/633183) about async function composition interesting. For composing functions of varying arity, [this Q&A](https://stackoverflow.com/a/42166494/633183). Combined, these might answer your question. – Mulan Feb 22 '19 at 20:15

2 Answers2

4

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)) 
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • May I follow up and ask how would you replace my `asyncPipe` with `pipeWith` and `then`? I experimented quite a lot, but couldn't figure out how. – J. Hesters Feb 21 '19 at 12:24
  • @J.Hesters: so long as the first function in the list returns a `Promise` (and if it is written with `async` it will) then you can just replace `asyncPipe` with `pipeWith(then)` and wrap the arguments to it in an array. (We'd like to eventually move `pipe` and `compose` to also use an array rather than varargs, but that's a large breaking change.) – Scott Sauyet Feb 21 '19 at 18:48
  • I think I got it. You mean that my `asyncPipe( // ... someFunctions getHouseList )` is basically equivalent to `pipeWith(then)([// ... someFunctions, getHouseList])`. – J. Hesters Feb 21 '19 at 18:58
  • 1
    @J.Hesters: exactly. – Scott Sauyet Feb 21 '19 at 19:06
1

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>
Hitmands
  • 13,491
  • 4
  • 34
  • 69
  • `R.gt(R.__, 1)` this doen't work: https://ramdajs.com/repl/#?const%20list%20%3D%20%5B1%2C%202%2C%203%5D%3B%0A%0Aconst%20check%20%3D%20R.pipe%28R.length%2C%20R.gt%28R._%2C%201%29%29%3B%0A%0Acheck%28list%29 – J. Hesters Feb 22 '19 at 13:54