3
\$\begingroup\$

I have a durations as such:

[{'duration': 3600}, {'duration': 3600}]

And I want the output to be {3600: 2}, where 2 is the number of occurrences of 3600.

My first attempt is use a loop, as such:

    var count = {};
    for (var d of durations) {
        var length = d.duration;
        if (length in count) {
            count[length] += 1;
        }
        else {
            count[length] = 1;
        }
    }
    console.log(count);

The second attempt uses reduce() in lodash:

    function foo(result, value) {
        var length = value.duration;

        if (length in result) {
            result[length] += 1;
        }
        else {
            result[length] = 1;
        }

        return result;
    }
    var reduced = _.reduce(durations, foo, {});
    console.log(reduced);

As can be seen, this second attempt is still as verbose as before.

Is there a way to write the iteratee function foo more conform to functional programming?

\$\endgroup\$
3
\$\begingroup\$

It's not really clear what you mean by "functional".

The use of reduce is nifty, and correctly done, but it seem like cargo cult programming. The realization that a lot of loop-like actions can be represented in terms of reduce (fold, aggregate, etc) is important to functional programing, but it should no more be your first choice than a while-loop.

The fact that it's verbose isn't necessarily a problem. It suggests that you might be failing at a general goal:

Write what you mean.

One of the things that makes functional programming good is that it helps us do that.
Javascript has limited tools for directly declaring lists. What you mean to do is declare a Dictionary from the Set of duration values to the Count of the source array Filtered by the respective duration. Or even better you could use a grouping function.

You could bring in a library for the task, which may be a good idea, or you could compromise a little.

function getCounts(items, key){
  let c = {};
  for(let item of items){
    c[key(item)] = items.filter(function(i){
        return key(i) === key(item);
      }).length;
  }
  return c;
};
counts = getCounts(durations, function(i){ return i['duration']; })'

You'll notice that that's quite inefficient. We could make it less wasteful, but it'd be less terse.

\$\endgroup\$
  • 2
    \$\begingroup\$ The introduction of your answer is good, but the code is not since its complexity is \$\mathcal O(n^2)\$. \$\endgroup\$ – Roland Illig May 16 at 2:29
  • \$\begingroup\$ Yeah, don't actually use that code. \$\endgroup\$ – ShapeOfMatter May 16 at 14:33
2
\$\begingroup\$

To reduce the verboseness of the code, you should extract the main idea of that code into a function:

function histogram(data, key) {
    const count = {};
    for (let item of data) {
        const value = data[key];
        count[value] = (count[value] || 0) + 1;
    }
    return count;
}

That way you can write your code as:

console.log(histogram(durations, 'duration'));

I first tried a simple count[value]++, but that didn't work since undefined + 1 is still undefined. Therefore I had to use the || 0 trick.

\$\endgroup\$
1
\$\begingroup\$

First point

Don't add quotes around property names when defining JS objects.

[{'duration': 3600}, {'duration': 3600}]

// should be

[{duration: 3600}, {duration: 3600}]

You state

"And I want the output to be {3600: 2}, where 2 is the number of occurrences of 3600."

Which is not that clear. Going by your code I assume you want [{duration: 60}, {duration: 60}, , {duration: 10}] converted to {60:2, 10:1}

I will also assume that all array items contain an object with the property named duration

Using these assumptions for the rest of the answer.

Rewrite

Taking your first snippet and turning it into a function.

  • You don't need the if (length in count) {, you can just use if (count[d.duration]) {
  • The object count and d should be constants as you don't change them.
  • Using a ternary you can reduce the 6 lines of the if else to one line.

Code

function countDurations(durations) {
    const counts = {};
    for (const {duration: len} of durations) {
        counts[len] = counts[len] ? counts[len] + 1 : 1;
    }
    return counts;
}

Or a little less verbose as a arrow function

const countDur = dur => dur.reduce((c, {duration: l}) => (c[l] = c[l]? c[l] + 1: 1, c), {});

or

const countDurs = durs => 
    durs.reduce((counts, {duration: len}) => 
        (counts[len] = counts[len] ? counts[len] + 1 : 1, counts)
    , {}
);

Rewrite 2

The second snippet is algorithmicly the same, just uses a different iteration method.

  • JavaScript has Array.reduce so you don't need to use a external library.
  • Separating the iterator inner block into a function count

Code

const count= (cnt, {duration: len}) => (cnt[len] = cnt[len] ? cnt[len] + 1 : 1, cnt);
const countDurations = durations => durations.reduce(count, {});

Or encapsulating the count function via closure

const countDurations = (()=> {
    const count = (cnt, {duration: len}) => (cnt[len] = cnt[len] ? cnt[len] + 1 : 1, cnt);
    return durations => durations.reduce(count, {});
})();

Functional?

"Is there a way to write the iterate function foo more conform to functional programming?"

JavaScript is not designed to be a functional language (it is impossible to create pure functions) so It is best to use functional as a guide rather than a must.

Almost pure

The second rewrite is more along the functional approach, but functional purist would complain that the reducer function has side effects by modifying the counter. You can create a copy of the counts each call as follows..

const counter = (c, {duration: len}) => (c = {...c}, (c[len] = c[len] ? c[len] + 1 : 1), c);
const countDurations = durations => durations.reduce(counter, {});

However this add significant memory and processing overhead with zero benefit (apart from a functional pat on the back).

Note

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.