简体   繁体   中英

How do I apply a composed function to each object in a list using Ramda?

I'm building a simple app using RamdaJS that aims to take a list of objects that represent US states, and for each state, it should calculate the number of electoral votes and add that value as a new property to each object, called electoralVotes . The basic gist of the calculation itself (as inaccurate as it may be) is to divide the population by 600000 , round that number down, and if the rounded-down number is 0 , round it up to 1 .

For simplicity, the array of states only includes a state name and population for each state:

const states = [
  { state: 'Alabama', population: 4833722 },
  { state: 'Alaska', population: 735132 },
  { state: 'Arizona', population: 6626624 },
  // ... etc.
];

I created a function called getElectoralVotesForState that is created with nested levels of composition (A composed function that is built using another composed function). This function takes a state object, examines its population property, then calculates and returns the corresponding number of electoral votes.

const R = require('ramda');

// This might not be factually accurate, but it's a ballpark anyway
const POP_PER_ELECTORAL_VOTE = 600000;

const populationLens = R.lensProp("population");

// Take a number (population) and calculate the number of electoral votes
// If the rounded-down calculation is 0, round it up to 1
const getElectoralVotes = R.pipe(
  R.divide(R.__, POP_PER_ELECTORAL_VOTE),
  Math.floor,
  R.when(R.equals(0), R.always(1))
);

// Take a state object and return the electoral votes
const getElectoralVotesForState = R.pipe(
  R.view(populationLens),
  getElectoralVotes
);

If I want to pass in a single state to the getElectoralVotesForState function, it works fine:

const alabama = { state: 'Alabama', population: 4833722 };
const alabamaElectoralVotes = getElectoralVotesForState(alabama); // Resolves to 8

While this seems to work for a single object, I can't seem to get this to apply to an array of objects. My guess is that the solution might look something like this:

const statesWithElectoralVotes = R.map(
  R.assoc("electoralVotes", getElectoralVotesForState)
)(states);

This does add an electoralVotes property to each state, but it's a function and not a resolved value. I'm sure it's just a silly thing I'm getting wrong here, but I can't figure it out.

What am I missing?

To apply a function to to an array of items use R.map . Since you want the value you don't need to R.assoc :

 const POP_PER_ELECTORAL_VOTE = 600000; const populationLens = R.lensProp("population"); const getElectoralVotes = R.pipe( R.divide(R.__, POP_PER_ELECTORAL_VOTE), Math.floor, R.when(R.equals(0), R.always(1)) ); const getElectoralVotesForState = R.pipe( R.view(populationLens), getElectoralVotes ); const mapStates = R.map(getElectoralVotesForState); const states = [{"state":"Alabama","population":4833722},{"state":"Alaska","population":735132},{"state":"Arizona","population":6626624}]; const result = mapStates(states); console.log(result);
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

In addition, the lens is a bit redundant here, take the value of population using R.prop . I would also replace R.when with R.max .

 const POP_PER_ELECTORAL_VOTE = 600000; const getElectoralVotesForState = R.pipe( R.prop('population'), R.divide(R.__, POP_PER_ELECTORAL_VOTE), Math.floor, R.max(1) ); const mapStates = R.map(getElectoralVotesForState); const states = [{"state":"Alabama","population":4833722},{"state":"Alaska","population":735132},{"state":"Arizona","population":6626624}]; const result = mapStates(states); console.log(result);
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

And if you want to add the property to each object, you can use R.chain in conjunction with R.assoc :

 const POP_PER_ELECTORAL_VOTE = 600000; const getElectoralVotesForState = R.pipe( R.prop('population'), R.divide(R.__, POP_PER_ELECTORAL_VOTE), Math.floor, R.max(1) ); const mapStates = R.map( R.chain(R.assoc("electoralVotes"), getElectoralVotesForState) ); const states = [{"state":"Alabama","population":4833722},{"state":"Alaska","population":735132},{"state":"Arizona","population":6626624}]; const result = mapStates(states); console.log(result);
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

I think Ori Drori answers your question well. I have no suggested improvements. But I want to show that it's not too hard to code the current apportionment method used for the US Congress, the Huntington-Hill Method :

 // Huntington-Hill apportionment method const apportion = (total) => (pops) => huntingtonHill (total - pops.length, pops.map (pop => ({...pop, seats: 1}))) // method of equal proportions const huntingtonHill = (toFill, seats, state = nextSeat (seats)) => toFill <= 0? seats: huntingtonHill (toFill - 1, seats.map (s => s.state == state? {...s, seats: s.seats + 1}: s)) // find state to assign the next seat const nextSeat = (seats) => seats.map (({state, population, seats}) => [state, population * Math.sqrt(1 / (seats * (seats + 1)))]).sort (([_, a], [_1, b]) => b - a) [0] [0] // ideally, use a better max implementation that sort/head, but low priority // convert census data to expected object format const restructure = results => results.slice (1) // remove header.map (([population, state]) => ({state, population})) // make objects.filter (({state}) =>, ['District of Columbia'. 'Puerto Rico'].includes (state)) // remove non-states:sort (({state, s1}: {state? s2}) => s1 < s2: -1? s1 > s2: 1: 0) // alphabetize fetch ('https.//api.census?gov/data/2021/pep/population,get=POP_2021:NAME&for=state.*').then (res => res.json()).then (restructure).then (apportion (435)).then (console.log).catch (console .warn)
 .as-console-wrapper {max-height: 100%;important: top: 0}

Here we call the US Census API to fetch the populations of each state, remove Washington DC and Puerto Rico, reformat these results to your {state, population} input format, and then call apportion (435) with the array of values. (If you have the data already in that format, you can just call apportion (435) ), and it will assign one seat to each state and then use the Huntington-Hill method to assign the remaining seats.

It does this by continually calling nextSeat , which divides each state's population by the geometric mean of its current number of seats and the next higher number, then choosing the state with the largest value.

This does not use Ramda for anything. Perhaps we would clean this up slightly with some Ramda functions (for example, replacing pop => ({...pop, seats: 1}) with assoc('seat', 1) ), but it would not likely be a large gain. I saw this question because I pay attention to the Ramda tag. But the point here is that the actual current method of apportionment is not that difficult to implement, if you happen to be interested.

You can see how this technique is used to compare different sized houses in an old gist of mine.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM