Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce returns an array, not a value #44

Closed
aeisenberg opened this issue Jan 5, 2015 · 7 comments · Fixed by #127
Closed

Reduce returns an array, not a value #44

aeisenberg opened this issue Jan 5, 2015 · 7 comments · Fixed by #127

Comments

@aeisenberg
Copy link

This was mentioned in comments of other issues, but I think it warrants it's own issue. The reduce implementation in the tutorial returns an array, not an object, which goes against the JS spec. For those people coming to this tutorial who are already familiar with JavaScript, this is more than a little confusing.

I'd recommend either:

  1. Clarifying why the reduce implementation here is different from the one of the JS spec
  2. Changing the exercise so that reduce does return a single value.

Great tutorial! Lots of fun to go through the exercises.

@MrNice
Copy link

MrNice commented Mar 5, 2015

@aeisenberg
It appears that reduce returns an array in order to stay homomorphic (array -> array), which is a good thing. Maintaining homomorphism the the data structure allows us to abstract the data structure away, just as we do when we go from arrays to observables. Unfortunately, you probably don't want to reveal that when you reveal reduce - it's a bit too much at once.


The clarification would then be something like,

NOTE: The ES5 spec has reduce return a single value instead of an array with a single value.

You can ignore this difference for now, but our reduce behaves this way for the same reason you are forbidden from using bracket array access notation.


I could also be totally wrong. @jhusain ?

@despairblue
Copy link

👍

@webxl
Copy link

webxl commented Oct 22, 2015

@MrNice Can you please explain "our reduce behaves this way for the same reason you are forbidden from using bracket array access notation."

Great comment BTW, it inspired me to dive into the Array object a bit and clarify my understanding:

https://tonicdev.com/webxl/array-return-types/

Feedback welcome!

@MrNice
Copy link

MrNice commented Oct 22, 2015

@webxl I completed an old version of this tutorial, which forbid the use of array[0] access notation, because you should abstract away the collection's container. In this way, .map becomes an interface that can be implemented by custom data types (such as the observable type).

ES5 reduce doesn't allow chaining: [].reduce().map().reduce().map() doesn't work because [].reduce() will return a single value.

You can get around this limitation by forcing the reduce accumulator to be an array containing the value you're accumulating:

// Summation that can be chained
[1, 2].reduce(function(acc, val) {
  acc[0] = acc[0] + val;
  // but the reducing function MUST return an array
  // So the abstraction is very leaky
  return acc;
}).map(function(val) {
  // Doubling because we can
  return 2 * val;
}); // Returns 6

And we dislike leaky abstractions.

The reason we want to abstract over the container is simple: we want to be able to have different, more specialized containers than arrays. Observables are containers where elements are ordered like arrays, but instead of being available positionally, they're available over time. You can implement .map on a tree that spits out a new tree, or a .reduce on a graph that combines all the graph's nodes into a single graph node. Note, however, with more complicated structures you must be somewhat aware of element traversal order.

I wonder what a tree over time looks like / is good for...

Let me know if that helps! We're trying to lay solid abstractions over familiar concepts, so that we can get away from the machine and into the land of thought stuffs.

@MrNice
Copy link

MrNice commented Oct 22, 2015

My code above doesn't work because I forgot how reduce works >.<.

[1, 2].reduce(function(acc, val) {
  // Without using a default array start value EVERY TIME, we must do this check
  if (Array.isArray(acc)) {
      acc[0] = acc[0] + val;
  } else {
      acc = [acc + val];
  }
  // but the reducing function MUST return an array
  // So the abstraction is very leaky
  return acc;
}).map(function(val) {
  // Doubling because we can
  return 2 * val;
}); // Returns [ 6 ]

Note the other form is this:

[1, 2].reduce(function(acc, val) {
  acc[0] = acc[0] + val;
  return acc;
}, [0]).map(function(val) {
  return 2 * val;
}); // Returns [ 6 ]

I find that hunting for missing [ ] is a pain, as is providing a sane default value, in this case the empty number. If this were multiplication, I'd need to use 1.

@webxl
Copy link

webxl commented Oct 22, 2015

@MrNice Thanks for that explanation.

I've only ever worked with this definition: "The reduce() method applies a function against an accumulator and each value of the array (from left-to-right) to reduce it to a single value."

I get your concerns with leaky abstractions, but I think it's confusing to reactive newbies like me if that definition above has changed to mean something else. Do you know the origins of reduce? Why not call the array-returning version fold? When I think "fold", like a folding fan, all portions of the paper are collapsed to the same 2d area, but there's still some characteristics of the expanded fan. While not very different, "reduce" might be forever associated with the " to a single value"/ map-and-reduce (in that order only) usage.

@MrNice
Copy link

MrNice commented Oct 23, 2015

@webxl Let's back up a bit

Your reduce definition, "The reduce() method applies a function against an accumulator and each value of the array (from left-to-right) to reduce it to a single value," describes an _implementation_ of reduce, as a method on JS arrays. We really want to be talking about the abstract notion of what a reducing operation should look like for a given data type.

The entire goal of this walkthrough is to introduce a new data type, the observable. The observable is interesting because it's a JS data type implemented in JS, while many JS programmers just use the 8 or so that every JS engine has (:heavy_plus_sign: the jQuery monad :stuck_out_tongue_winking_eye:). The observable is powerful because it allows you to compose (through pipelining) value transformations over time. Importantly, these compositions are simple to build, read, and analyze, simply because they use a monadic chaining API - every method call returns an observable, all observables implement the same methods, so you can chain method calls together to make a single observable that is influenced by all of the previous behaviors.

That's a lot to grok, so the tutorial starts with the common JS array, and leads you through building out the foundational knowledge you need to feel comfortable with the observable interface.

The first step of which requires implementing a reduce method. Technically, all a reduce operation has to do is turn a logical collection (set of similar elements) into a single element. But if we want to be able to pipeline these operations, then the method has to return the same type it acts upon, an array. In order to get the final value, you just need to access the data, which in this case looks like a[0], akin to underscore's .value() method.

We are allowed to diverge in our implementation because we're really chasing after the abstraction. We just want to understand the concept of reducing well enough that we can talk about observables, and the fewer conceptual differences, the easier it is to follow along.

Now, about fold vs reduce...

They are synonyms, and most languages use the word reduce.

As far as I can tell from looking at which languages implemented fold / reduce and when, it appears that it was first named aggregate, but in Common Lisp it was named reduce, and that's what most languages use today. But many mathy languages, like Mathematica, Erlang and Scala call it Fold or even more precisely, FoldL.

Most of the time, the order of data inputs doesn't matter during a reduction operation (unless the data isn't timestamped on its own and you're doing something like a trending feed, or your operation isn't associative, like exponentiation). So reducing a shuffled collection should return the same value.

More mathy / typey languages care about being very precise. When you reduce an array, you do so by folding. This folding operation then has a direction, left or right. This provides the most clarity to the programmer, and has an excellent visual metaphor, whereas reducing is a chemistry / cooking metaphor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants