Skip to content

Instantly share code, notes, and snippets.

@ChrisWhealy
Last active November 9, 2019 13:32
Show Gist options
  • Save ChrisWhealy/b8c873a7753fc17ce0131c654caf60fa to your computer and use it in GitHub Desktop.
Save ChrisWhealy/b8c873a7753fc17ce0131c654caf60fa to your computer and use it in GitHub Desktop.
JavaScript function to partition an array by passing each element through a predicate function
/***********************************************************************************************************************
* Partition an array into two arrays based on passing each element to a predicate function.
*
* This function is designed to extend the Array prototype, but only by the explicit action of the consuming library.
* E.G. Assuming the function below belongs to a "utils" module, you must write something like the following:
*
* const utils = require('./utils.js')
* Array.prototype.partitionWith = utils.partitionWith
*
* This function must be defined using a traditional "function" declaration instead of an arrow function. If an arrow
* function were used, then the reference to "this.reduce" wouldn't work...
*
* partitionWith takes a simple predicate function as an argument and returns an array containing two other arrays.
* The first array contains all the elements that do not match the predicate, and the second, all the elements that do
* match the predicate
*
* E.G.
*
* var isEven = x => x % 2 === 0
* var numbers = [1,2,3,4,5,6,7,8,9,10]
* var [odd, even] = numbers.partitionWith(isEven)
*
* odd = [1,3,5,7,9]
* even = [2,4,6,8,10]
*/
const partitionWith = function(predFn) {
const partitionReducer = (acc, el) =>
(success => success ? [acc[0], acc[1].concat(el)]
: [acc[0].concat(el), acc[1]])
(predFn(el))
return this.reduce(partitionReducer, [[],[]])
}
@qmacro
Copy link

qmacro commented Nov 8, 2019

Very nice, I also like the bonus example of array destructuring ([odd, even] = ...) in the comments.

I wrote a simple version, which however has the same essence - starting with [[], []] and using reduce.

Array.prototype.partition = function(f) {
  return this.reduce((a, x) => a[f(x) ? 0 : 1].push(x) && a, [[],[]])
}

Some questions:

  • Can you expand on why you chose to use an IIFE in the body of the reducer? So as to only have a single expression?
  • I also notice you're "constructing" the results in a clear and calm way (lines 28 and 29), and avoiding the use of push. Is that deliberate?
  • Is there a reason you decided on partitionWith and not partition?

@ChrisWhealy
Copy link
Author

Nice - the above implementation could certainly be condensed

You could even go one step further into the obscurity of "JavaScript type coercion land” with

Array.prototype.partitionWith = function(f) {
  return this.reduce((a, x) => a[f(x)+0].push(x) && a, [[],[]])
}

Since f() is a predicate function, we can coerce its Boolean return value to an integer by adding zero...

But this is heading towards write-only code...

:-P

@qmacro
Copy link

qmacro commented Nov 8, 2019

Yes, I thought the ternary operator expression as the array index was a fair "middle ground" between concise and unreadable code :)

@ChrisWhealy
Copy link
Author

And to answer your questions above...

  1. I try to use IIFEs wherever possible simply because I'm trying to conform to several academic principles:

    1. In Lambda Calculus, a function can only operate on the values it receives as arguments I.E. name binding only happens at the time a function is called, not at some intermediate stage within the function's coding. Under this governing principle, internal let declarations are impossible
      It must be remembered however that it is not always either easy or practical to write functions this way. Sometimes, a function's implementation would become so obscure if written this way, that is ceases to provide any practical value
    2. A function should do one thing and one thing only: and if that one thing can be represented as a single expression, then so much the better

    However, I've found following these principles to be something of a mixed blessing...

    • On the positive side, because an individual function contains so little coding, it is very easy to see what that function does
    • On the negative side, debugging such functions is a real pig if you are in the habit of using "meaning obscuring" names such as f or a or b

    Therefore, as a compromise, I've developed the style above in which all the internal variables within a function (whose values are bound at invocation time) are given meaningful, self-descriptive names. This gives you a fighting chance to see what's gone wrong when in debug mode

  2. Yes, the use of concat also allows for el itself to be an array

  3. It think the name partitionWith makes the function's purpose more self-descriptive - you're partitioning an array with (I.E. by the means of) the function supplied as an argument. I also lifted this function name directly out of the Erlang lists module

:-)

@qmacro
Copy link

qmacro commented Nov 8, 2019

Thanks Chris, I did think it might be related to academic principles (I know you very well!)

Interesting term, "meaning obscuring". I quite like f, a, b, x and xs and think that they help, rather than obscure. Theoretically they should also force (or at least persuade me to think about) splitting out logic into further separate functions if it all gets "a bit too much". But I know some folks see that differently. I subscribe to the Erik Meijer school of "x over xs".

partition vs partitionWith - interesting too - Ramda calls it partition. After a beer, I'd argue that the 'with' was superfluous, as (almost) "of course you're going to provide a predicate function" :-)

@ChrisWhealy
Copy link
Author

I have no problem with single cases of x over xs, but when you're looking at a reasonably deep stack trace containing lots of functions that all use single letter variable names like names like f and a and b, you (or more specifically, I) quickly loose track of what value is stored where. Quinsiquontly, in my attempt to remove any ambiguity, I tend to use longer, self-descriptive names

The preference of partitionWith over partition is motivated by the same reasoning

Alles sollte Idiotensicher sein, weil manchmal, bin Ich das Idiot...

:-P

@qmacro
Copy link

qmacro commented Nov 8, 2019

Goodness me you wouldn't believe how lovely Ramda's implementation of partition is. Might be worth a beer and a live stream to look at it...

@ChrisWhealy
Copy link
Author

Yes, the Ramda implementation is:

partition = juxt([filter, reject])

Where juxt first applies the filter function to produce the first array, then its logical complement reject to produce the second array

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