Functional way to get previous element during map

情到浓时终转凉″ 提交于 2019-12-24 11:19:40

问题


I have an array which I map over. I need to compare the current element with the previous. I am detecting if the current element is the same as the previous element by comparing their ids and doing something different based on this condition. Is there any purely functional way to do it without doing index math?

items.map((item, index) => {
    if(item.id === items[index - 1 > 0 ? index - 1 : 0].id) {
    // do something
} else {
   // do something else
 }
})

The code works but I would like to avoid doing math on the index. Is there any way to do it?


回答1:


Are you sure that you want a map? This sounds like an XY problem. If you want to map over adjacent elements of an array then you'd have to define your own function.

const mapAdjacent = (mapping, array) => {
    const {length} = array, size = length - 1, result = new Array(size);
    for (let i = 0; i < size; i++) result[i] = mapping(array[i], array[i + 1]);
    return result;
};

const items = [1, 2, 3, 4, 5];

const result = mapAdjacent((x, y) => [x, y], items);

console.log(result); // [[1, 2], [2, 3], [3, 4], [4, 5]]

Note that this will throw a RangeError if you give it an empty array as input.

const mapAdjacent = (mapping, array) => {
    const {length} = array, size = length - 1, result = new Array(size);
    for (let i = 0; i < size; i++) result[i] = mapping(array[i], array[i + 1]);
    return result;
};

const items = [];

const result = mapAdjacent((x, y) => [x, y], items); // RangeError: Invalid array length

console.log(result);

I think this is good behaviour because you shouldn't be giving mapAdjacent an empty array to begin with.


Here's a purely functional implementation of mapAdjacent which uses reduceRight. As an added bonus, it works for any iterable object.

const mapAdjacent = (mapping, [head, ...tail]) =>
    tail.reduceRight((recur, item) => prev =>
        [mapping(prev, item), ...recur(item)]
      , _ => [])(head);

const items = "hello";

const result = mapAdjacent((x, y) => [x, y], items);

console.log(result); // [['h', 'e'], ['e', 'l'], ['l', 'l'], ['l', 'o']]

Unlike the iterative version, it returns an empty array instead of throwing an error if you give it an empty array as input.

const mapAdjacent = (mapping, [head, ...tail]) =>
    tail.reduceRight((recur, item) => prev =>
        [mapping(prev, item), ...recur(item)]
      , _ => [])(head);

const items = "";

const result = mapAdjacent((x, y) => [x, y], items);

console.log(result); // []

Note that this is an unintended side effect of array destructuring with rest elements in JavaScript. The equivalent Haskell version does raise an exception.

mapAdjacent :: (a -> a -> b) -> [a] -> [b]
mapAdjacent f (x:xs) = foldr (\y g x -> f x y : g y) (const []) xs x

main :: IO ()
main = do
    print $ mapAdjacent (,) "hello" -- [('h','e'),('e','l'),('l','l'),('l','o')]
    print $ mapAdjacent (,) "" -- Exception: Non-exhaustive patterns in function mapAdjacent

However, returning an empty array might be desirable for this function. It's equivalent to adding the mapAdjacent f [] = [] case in Haskell.




回答2:


Not a particularly fast implementation, but destructuring assignment makes it particularly elegant -

const None =
  Symbol ()

const mapAdjacent = (f, [ a = None, b = None, ...more ] = []) =>
  a === None || b === None
    ? []
    : [ f (a, b), ...mapAdjacent (f, [ b, ...more ]) ]

const pair = (a, b) =>
  [ a, b ]

console.log(mapAdjacent(pair, [ 1, 2, 3 ]))
// [ [ 1, 2 ], [ 2, 3 ] ]

console.log(mapAdjacent(pair, "hello"))
// [ [ h, e ], [ e, l ], [ l, l ], [ l, o ] ]

console.log(mapAdjacent(pair, [ 1 ]))
// []

console.log(mapAdjacent(pair, []))
// []

Or write it as a generator -

const mapAdjacent = function* (f, iter = [])
{ while (iter.length > 1)
  { yield f (...iter.slice(0,2))
    iter = iter.slice(1)
  }
}

const pair = (a, b) =>
  [ a, b ]

console.log(Array.from(mapAdjacent(pair, [ 1, 2, 3 ])))
// [ [ 1, 2 ], [ 2, 3 ] ]

console.log(Array.from(mapAdjacent(pair, "hello")))
// [ [ h, e ], [ e, l ], [ l, l ], [ l, o ] ]

console.log(Array.from(mapAdjacent(pair, [ 1 ])))
// []

console.log(Array.from(mapAdjacent(pair, [])))
// []



回答3:


The reduce() function provides a functional what you need:

items.reduce((previousValue, currentValue) => {
  if(currentValue.id === previousValue.id) {
    // do something
  } else {
    // do something else
  }
});



回答4:


As I mentioned in a comment, I would suggest using reduce. Here is an example:

const input = [
  {id: 1, value: "Apple Turnover"},
  {id: 1, value: "Apple Turnover"},
  {id: 2, value: "Banana Bread"},
  {id: 3, value: "Chocolate"},
  {id: 3, value: "Chocolate"},
  {id: 3, value: "Chocolate"},
  {id: 1, value: "Apple"},
  {id: 4, value: "Danish"},
];

// Desired output: Array of strings equal to values in the above array,
// but with a prefix string of "New: " or "Repeated: " depending on whether
// the id is repeated or not

const reducer = (accumulator, currentValue) => {
  let previousValue, descriptions, isRepeatedFromPrevious;
  
  if (accumulator) {
    previousValue = accumulator.previousValue;
    descriptions = accumulator.descriptions;
    isRepeatedFromPrevious = previousValue.id === currentValue.id;
  } else {
    descriptions = [];
    isRepeatedFromPrevious = false;
  }
  
  if (isRepeatedFromPrevious) {
    // The following line is not purely functional and performs a mutation,
    // but maybe we do not care because the mutated object did not exist
    // before this reducer ran.
    descriptions.push("Repeated: " + currentValue.value);
  } else {
    // Again, this line is mutative
    descriptions.push("New: " + currentValue.value);
  }
	
  return { previousValue: currentValue, descriptions }
};


const output = input.reduce(reducer, null).descriptions;

document.getElementById('output').innerText = JSON.stringify(output);
<output id=output></output>


来源:https://stackoverflow.com/questions/57767346/functional-way-to-get-previous-element-during-map

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!