-->

Ramda: Is there a way to 'fork' a paramete

2019-05-27 05:04发布

问题:

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.

回答1:

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)) 


回答2:

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>