5

I have two array object as following:

var arr1 = [
    {
        name: 1,
        value: 10
    },
    {
        name: 2,
        value: 15
    }
]

var arr2 = [
    {
        name: 3,
        value: 5
    },
    {
        name: 4,
        value: 3
    }
]

I want to redefine the key and reduce each data with the same index.

output:

var arr1 = [
    {
        itemLabel: 1,
        itemValue: 5
    }, 
    {
        itemLabel: 2,
        itemValue: 12
    }
]

I'm doing now as following:

formatData = arr1.map((row, index) => ({
    itemLabel: arr1.name,
    itemValue: arr1.value - arr2[index].value
}))

Is there any better solution of doing this?

KevinHu
  • 991
  • 3
  • 10
  • 20
  • 4
    What's the issue with your current solution and what do you mean by 'more functional' ? – klugjo Jan 05 '18 at 09:08
  • 1
    I think his mean, 'is there any better solution' – Muhammet Can TONBUL Jan 05 '18 at 09:12
  • 3
    Your `map` call is already a pure function. It does not mutate any existing objects or cause any side-effects. It cannot be *more functional*. You could do it another way, but it wouldn't be any better. – CodingIntrigue Jan 05 '18 at 09:12
  • What you do is the better solution – Faly Jan 05 '18 at 09:13
  • mm, I see. Is there any better solution !? – KevinHu Jan 05 '18 at 09:13
  • @CodingIntrigue It can be _more functional_ because the anonymous function isn't pure. It depends on `arr1` and `arr2`. – Roman Jan 05 '18 at 10:14
  • @Roman Since we only have the snippet, not the full context, we don't know the signature isn't `const fn = (arr1, arr2) => { let formatData; formatData = arr1.map((row, index) => ...); return formatData; }` - that would be a pure function, right? – CodingIntrigue Jan 05 '18 at 10:16
  • @CodingIntrigue jup. and even shorter with `const fn = (arr1, arr2) => arr1.map((row, index) => ...)` – Roman Jan 05 '18 at 10:20
  • I have no idea to choose the best answer..you guys are impressive! – KevinHu Jan 05 '18 at 10:32

4 Answers4

1

Your code is just fine, you could use recursion as well:

var arr1 =[{
  name: 1,
  value: 10 
}, {
  name: 2,
  value: 15
}];

var arr2= [{
  name: 3,
  value: 5 
}, {
  name: 4,
  value: 3
}]


const createObject=(arr1,arr2,ret=[])=>{
  if(arr1.length!==arr2.length){
    throw("Arrays should be the same length.")
  }
  const item = {
    itemLabel: arr1[0].name,
    itemValue: arr1[0].value - arr2[0].value
  };
  if(arr1.length===0){
    return ret;
  };
  return createObject(arr1.slice(1),arr2.slice(1),ret.concat(item));
}
console.log(createObject(arr1,arr2));

Both functions implementing a map or reduce would have to use either arr1 or arr2 outside of their scope (not passed to it as parameter) so strictly speaking not pure. But you could easily solve it with partial application:

var arr1 =[{
  name: 1,
  value: 10 
}, {
  name: 2,
  value: 15
}];

var arr2= [{
  name: 3,
  value: 5 
}, {
  name: 4,
  value: 3
}];


const mapFunction = arr2 => (item,index) => {
  return {
    itemLabel: item.name,
    itemValue: item.value - arr2[index].value
  }
}

var createObject=(arr1,arr2,ret=[])=>{
  if(arr1.length!==arr2.length){
    throw("Arrays should be the same length.")
  }
  const mf = mapFunction(arr2);
  return arr1.map(mf);
}
console.log(createObject(arr1,arr2));

But as CodingIntrigue mentioned in the comment: none of these are any "better" than you've already done.

HMR
  • 37,593
  • 24
  • 91
  • 160
1

One-man army

A simple recursive program that handles everything in a single function. There's a clear mixture of concerns here which hurts of function's overall readability. We'll see one such remedy for this problem below

const main = ([x, ...xs], [y, ...ys]) =>
  x === undefined || y === undefined
    ? []
    : [ { itemLabel: x.name, itemValue: x.value - y.value } ] .concat (main (xs, ys))

const arr1 =
  [ { name: 1, value: 10 }, { name: 2, value: 15 } ]

const arr2 =
  [ { name: 3, value: 5 }, { name: 4, value: 3 } ]
  
console.log (main (arr1, arr2))
// [ { itemLabel: 1, itemValue: 5 },
//   { itemLabel: 2, itemValue: 12 } ]

Thinking with types

This part of the answer is influenced by type theory from the Monoid category – I won't go too far into it because I think the code should be able to demonstrate itself.

So we have two types in our problem: We'll call them Foo and Bar

  • Foo – has name, and value fields
  • Bar – has itemLabel and itemValue fields

We can represent our "types" however we want, but I chose a simple function which constructs an object

const Foo = (name, value) =>
  ({ name
   , value
   })

const Bar = (itemLabel, itemValue) =>
  ({ itemLabel
   , itemValue
   })

Making values of a type

To construct new values of our type, we just apply our functions to the field values

const arr1 =
  [ Foo (1, 10), Foo (2, 15) ]

const arr2 =
  [ Foo (3, 5), Foo (4, 3) ]

Let's see the data we have so far

console.log (arr1)
// [ { name: 1, value: 10 },
//   { name: 2, value: 15 } ]

console.log (arr2)
// [ { name: 3, value: 5 },
//   { name: 4, value: 3 } ]

Some high-level planning

We're off to a great start. We have two arrays of Foo values. Our objective is to work through the two arrays by taking one Foo value from each array, combining them (more on this later), and then moving onto the next pair

const zip = ([ x, ...xs ], [ y, ...ys ]) =>
  x === undefined || y === undefined
    ? []
    : [ [ x, y ] ] .concat (zip (xs, ys))

console.log (zip (arr1, arr2))
// [ [ { name: 1, value: 10 },
//     { name: 3, value: 5 } ],
//   [ { name: 2, value: 15 },
//     { name: 4, value: 3 } ] ]

Combining values: concat

With the Foo values properly grouped together, we can now focus more on what that combining process is. Here, I'm going to define a generic concat and then implement it on our Foo type

// generic concat
const concat = (m1, m2) =>
  m1.concat (m2)

const Foo = (name, value) =>
  ({ name
   , value
   , concat: ({name:_, value:value2}) =>
       // keep the name from the first, subtract value2 from value
       Foo (name, value - value2)
   })

console.log (concat (Foo (1, 10), Foo (3, 5)))
// { name: 1, value: 5, concat: [Function] }

Does concat sound familiar? Array and String are also Monoid types!

concat ([ 1, 2 ], [ 3, 4 ])
// [ 1, 2, 3, 4 ]

concat ('foo', 'bar')
// 'foobar'

Higher-order functions

So now we have a way to combine two Foo values together. The name of the first Foo is kept, and the value properties are subtracted. Now we apply this to each pair in our "zipped" result. Functional programmers love higher-order functions, so you'll appreciate this higher-order harmony

const apply = f => xs =>
  f (...xs)

zip (arr1, arr2) .map (apply (concat))
// [ { name: 1, value: 5, concat: [Function] },
//   { name: 2, value: 12, concat: [Function] } ]

Transforming types

So now we have the Foo values with the correct name and value values, but we want our final answer to be Bar values. A specialized constructor is all we need

Bar.fromFoo = ({ name, value }) =>
  Bar (name, value)

Bar.fromFoo (Foo (1,2))
// { itemLabel: 1, itemValue: 2 }

zip (arr1, arr2)
  .map (apply (concat))
  .map (Bar.fromFoo)
// [ { itemLabel: 1, itemValue: 5 },
//   { itemLabel: 2, itemValue: 12 } ]

Hard work pays off

A beautiful, pure functional expression. Our program reads very nicely; flow and transformation of the data is easy to follow thanks to the declarative style.

// main :: ([Foo], [Foo]) -> [Bar]
const main = (xs, ys) =>
  zip (xs, ys)
    .map (apply (concat))
    .map (Bar.fromFoo)

And a complete code demo, of course

const Foo = (name, value) =>
  ({ name
   , value
   , concat: ({name:_, value:value2}) =>
       Foo (name, value - value2)
   })
  
const Bar = (itemLabel, itemValue) =>
  ({ itemLabel
   , itemValue
   })

Bar.fromFoo = ({ name, value }) =>
  Bar (name, value)

const concat = (m1, m2) =>
  m1.concat (m2)
  
const apply = f => xs =>
  f (...xs)  

const zip = ([ x, ...xs ], [ y, ...ys ]) =>
  x === undefined || y === undefined
    ? []
    : [ [ x, y ] ] .concat (zip (xs, ys))

const main = (xs, ys) =>
  zip (xs, ys)
    .map (apply (concat))
    .map (Bar.fromFoo)

const arr1 =
  [ Foo (1, 10), Foo (2, 15) ]
  
const arr2 =
  [ Foo (3, 5), Foo (4, 3) ]

console.log (main (arr1, arr2))
// [ { itemLabel: 1, itemValue: 5 },
//   { itemLabel: 2, itemValue: 12 } ]

Remarks

Our program above is implemented with a .map-.map chain which means handling and creating intermediate values multiple times. We also created an intermediate array of [[x1,y1], [x2,y2], ...] in our call to zip. Category theory gives us things like equational reasoning so we could replace m.map(f).map(g) with m.map(compose(f,g)) and achieve the same result. So there's room to improve this yet, but I think this is just enough to cut your teeth and start thinking about things in a different way.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    Really informative to read this answer that, in a way, uses the same approach as my answer but is clearer, uses the correct terminology and is better written ;) I've learnt that my `pairs` is called `zip`, that my `merge` is essentially `concat`, and that my `helper that "applies"` can actually be replaced by a literal `apply` function. – user3297291 Jan 06 '18 at 13:39
0

To make your solution more functional you need to change your anonymous function to a pure (anonymous) function.

A pure function is a function that, given the same input, will always return the same output

The anonymous function depends on the mutable variable arr1 and arr2. That means that it depends on the system state. So it doesn't fit into the pure function rule.

The following is maybe not the best implementaion but I hope it gives you an idea..

Let's Make it Pure

To make it pure we can pass the variables into the function as arguments

const mapWithObject = (obj2, obj1, index) => ({
    itemLabel: obj1.name,
    itemValue: obj1.value - obj2[index].value
})

// example call
const result = mapWithObject(arr2, arr1[0], 0)

Ok, but now the function doesn't fit into map anymore because it takes 3 arguments instead of 2..

Let's Curry it

const mapWithObject = obj2 => (obj1, index) => ({
  itemLabel: obj1.name,
  itemValue: obj1.value - obj2[index].value
})

const mapObject_with_arr2 = mapWithObject(arr2)

// example call
const result = mapObject_with_arr2(arr1[0], 0)

Full Code

const arr1 = [{
    name: 1,
    value: 10
  },
  {
    name: 2,
    value: 15
  }
]

const arr2 = [{
    name: 3,
    value: 5
  },
  {
    name: 4,
    value: 3
  }
]

const mapWithObject = obj2 => (obj1, index) => ({
  itemLabel: obj1.name,
  itemValue: obj1.value - obj2[index].value
})

const mapObject_with_arr2 = mapWithObject(arr2)

const mappedObject = arr1.map(mapObject_with_arr2)

console.log(mappedObject)
Roman
  • 4,922
  • 3
  • 22
  • 31
0

If you don't care to much about performance, but want to separate your concerns a bit further you could use this approach:

  • Define a function that does the "pairing" between arr1 and arr2

    [a, b, c] + [1, 2, 3] -> [ [ a, 1 ], [ b, 2 ], [ c, 3 ] ]
    
  • Define a function that clearly shows the merge strategy of two objects

    { a: 1, b: 10 } + { a: 2, b: 20 } -> { a: 1, b: -10 } 
    
  • Define simple helpers that compose the two so you can pass your original arrays and be returned the desired output in one function call.

Here's an example:

var arr1=[{name:1,value:10},{name:2,value:15}],arr2=[{name:3,value:5},{name:4,value:3}];

// This is a very general method that bundles two
// arrays in an array of pairs. Put it in your utils
// and you can use it everywhere
const pairs = (arr1, arr2) => Array.from(
  { length: Math.max(arr1.length, arr2.length) },
  (_, i) => [ arr1[i], arr2[i] ]
);

// This defines our merge strategy for two objects.
// Ideally, you should give it a better name, based
// on what the objects represent
const merge = 
  (base, ext) => ({ 
    itemLabel: base.name,
    itemValue: base.value - ext.value
  });

// This is a helper that "applies" our merge method 
// to an array of two items.
const mergePair = ([ base, ext ]) => merge(base, ext);

// Another helper that composes `pairs` and `mergePair` 
// to allow you to pass your original data.
const mergeArrays = (arr1, arr2) => pairs(arr1, arr2).map(mergePair);

console.log(mergeArrays(arr1, arr2));
user3297291
  • 22,592
  • 4
  • 29
  • 45