The Practical Way
I think it's wrong to say a particular implementation is "The Right Way™" if it's only "right" ("correct") in contrast to a "wrong" solution. Tomáš's solution is a clear improvement over string-based array comparison, but that doesn't mean it's objectively "right". What is right anyway? Is it the fastest? Is it the most flexible? Is it the easiest to comprehend? Is it the quickest to debug? Does it use the least operations? Does it have any side effects? No one solution can have the best of all the things.
Tomáš's could say his solution is fast but I would also say it is needlessly complicated. It tries to be an all-in-one solution that works for all arrays, nested or not. In fact, it even accepts more than just arrays as an input and still attempts to give a "valid" answer.
Generics offer reusability
My answer will approach the problem differently. I'll start with a generic arrayCompare
procedure that is only concerned with stepping through the arrays. From there, we'll build our other basic comparison functions like arrayEqual
and arrayDeepEqual
, etc
// arrayCompare :: (a -> a -> Bool) -> [a] -> [a] -> Bool
const arrayCompare = f => ([x,...xs]) => ([y,...ys]) =>
x === undefined && y === undefined
? true
: Boolean (f (x) (y)) && arrayCompare (f) (xs) (ys)
In my opinion, the best kind of code doesn't even need comments, and this is no exception. There's so little happening here that you can understand the behaviour of this procedure with almost no effort at all. Sure, some of the ES6 syntax might seem foreign to you now, but that's only because ES6 is relatively new.
As the type suggests, arrayCompare
takes comparison function, f
, and two input arrays, xs
and ys
. For the most part, all we do is call f (x) (y)
for each element in the input arrays. We return an early false
if the user-defined f
returns false
– thanks to &&
's short-circuit evaluation. So yes, this means the comparator can stop iteration early and prevent looping through the rest of the input array when unnecessary.
Strict comparison
Next, using our arrayCompare
function, we can easily create other functions we might need. We'll start with the elementary arrayEqual
…
// equal :: a -> a -> Bool
const equal = x => y =>
x === y // notice: triple equal
// arrayEqual :: [a] -> [a] -> Bool
const arrayEqual =
arrayCompare (equal)
const xs = [1,2,3]
const ys = [1,2,3]
console.log (arrayEqual (xs) (ys)) //=> true
// (1 === 1) && (2 === 2) && (3 === 3) //=> true
const zs = ['1','2','3']
console.log (arrayEqual (xs) (zs)) //=> false
// (1 === '1') //=> false
Simple as that. arrayEqual
can be defined with arrayCompare
and a comparator function that compares a
to b
using ===
(for strict equality).
Notice that we also define equal
as it's own function. This highlights the role of arrayCompare
as a higher-order function to utilize our first order comparator in the context of another data type (Array).
Loose comparison
We could just as easily defined arrayLooseEqual
using a ==
instead. Now when comparing 1
(Number) to '1'
(String), the result will be true
…
// looseEqual :: a -> a -> Bool
const looseEqual = x => y =>
x == y // notice: double equal
// arrayLooseEqual :: [a] -> [a] -> Bool
const arrayLooseEqual =
arrayCompare (looseEqual)
const xs = [1,2,3]
const ys = ['1','2','3']
console.log (arrayLooseEqual (xs) (ys)) //=> true
// (1 == '1') && (2 == '2') && (3 == '3') //=> true
Deep comparison (recursive)
You've probably noticed that this is only shallow comparison tho. Surely Tomáš's solution is "The Right Way™" because it does implicit deep comparison, right ?
Well our arrayCompare
procedure is versatile enough to use in a way that makes a deep equality test a breeze …
// isArray :: a -> Bool
const isArray =
Array.isArray
// arrayDeepCompare :: (a -> a -> Bool) -> [a] -> [a] -> Bool
const arrayDeepCompare = f =>
arrayCompare (a => b =>
isArray (a) && isArray (b)
? arrayDeepCompare (f) (a) (b)
: f (a) (b))
const xs = [1,[2,[3]]]
const ys = [1,[2,['3']]]
console.log (arrayDeepCompare (equal) (xs) (ys)) //=> false
// (1 === 1) && (2 === 2) && (3 === '3') //=> false
console.log (arrayDeepCompare (looseEqual) (xs) (ys)) //=> true
// (1 == 1) && (2 == 2) && (3 == '3') //=> true
Simple as that. We build a deep comparator using another higher-order function. This time we're wrapping arrayCompare
using a custom comparator that will check if a
and b
are arrays. If so, reapply arrayDeepCompare
otherwise compare a
and b
to the user-specified comparator (f
). This allows us to keep the deep comparison behavior separate from how we actually compare the individual elements. Ie, like the example above shows, we can deep compare using equal
, looseEqual
, or any other comparator we make.
Because arrayDeepCompare
is curried, we can partially apply it like we did in the previous examples too
// arrayDeepEqual :: [a] -> [a] -> Bool
const arrayDeepEqual =
arrayDeepCompare (equal)
// arrayDeepLooseEqual :: [a] -> [a] -> Bool
const arrayDeepLooseEqual =
arrayDeepCompare (looseEqual)
To me, this already a clear improvement over Tomáš's solution because I can explicitly choose a shallow or deep comparison for my arrays, as needed.
Object comparison (example)
Now what if you have an array of objects or something ? Maybe you want to consider those arrays as "equal" if each object has the same id
value …
// idEqual :: {id: Number} -> {id: Number} -> Bool
const idEqual = x => y =>
x.id !== undefined && x.id === y.id
// arrayIdEqual :: [a] -> [a] -> Bool
const arrayIdEqual =
arrayCompare (idEqual)
const xs = [{id:1}, {id:2}]
const ys = [{id:1}, {id:2}]
console.log (arrayIdEqual (xs) (ys)) //=> true
// (1 === 1) && (2 === 2) //=> true
const zs = [{id:1}, {id:6}]
console.log (arrayIdEqual (xs) (zs)) //=> false
// (1 === 1) && (2 === 6) //=> false
Simple as that. Here I've used vanilla JS objects, but this type of comparator could work for any object type; even your custom objects. Tomáš's solution would need to be completely reworked to support this kind of equality test
Deep array with objects? Not a problem. We built highly versatile, generic functions, so they'll work in a wide variety of use cases.
const xs = [{id:1}, [{id:2}]]
const ys = [{id:1}, [{id:2}]]
console.log (arrayCompare (idEqual) (xs) (ys)) //=> false
console.log (arrayDeepCompare (idEqual) (xs) (ys)) //=> true
Arbitrary comparison (example)
Or what if you wanted to do some other kind of kind of completely arbitrary comparison ? Maybe I want to know if each x
is greater than each y
…
// gt :: Number -> Number -> Bool
const gt = x => y =>
x > y
// arrayGt :: [a] -> [a] -> Bool
const arrayGt = arrayCompare (gt)
const xs = [5,10,20]
const ys = [2,4,8]
console.log (arrayGt (xs) (ys)) //=> true
// (5 > 2) && (10 > 4) && (20 > 8) //=> true
const zs = [6,12,24]
console.log (arrayGt (xs) (zs)) //=> false
// (5 > 6) //=> false
Less is More
You can see we're actually doing more with less code. There's nothing complicated about arrayCompare
itself and each of the custom comparators we've made have a very simple implementation.
With ease, we can define exactly how we wish for two arrays to be compared — shallow, deep, strict, loose, some object property, or some arbitrary computation, or any combination of these — all using one procedure, arrayCompare
. Maybe even dream up a RegExp
comparator ! I know how kids love those regexps …
Is it the fastest? Nope. But it probably doesn't need to be either. If speed is the only metric used to measure the quality of our code, a lot of really great code would get thrown away — That's why I'm calling this approach The Practical Way. Or maybe to be more fair, A Practical Way. This description is suitable for this answer because I'm not saying this answer is only practical in comparison to some other answer; it is objectively true. We've attained a high degree of practicality with very little code that's very easy to reason about. No other code can say we haven't earned this description.
Does that make it the "right" solution for you ? That's up for you to decide. And no one else can do that for you; only you know what your needs are. In almost all cases, I value straightforward, practical, and versatile code over clever and fast kind. What you value might differ, so pick what works for you.
Edit
My old answer was more focused on decomposing arrayEqual
into tiny procedures. It's an interesting exercise, but not really the best (most practical) way to approach this problem. If you're interested, you can see this revision history.
JSON.parse
would also iterate through each value anyway so I guess it would be better to compare iterating through each value and reduce some steps of execution ( like encoding it into JSON ). – Allay([] == []) == false
. – Recapitulate[1, 2].join() === [1, 2].join()
– Fraughtif (a1.length!==a2.length || JSON.stringify(a1)!==JSON.stringify(a2)) { /* something is different */ }
– Bantuis
and==
, which do "what you'd expect them to do," one checks reference equality, and the other uses either built-in or overloaded comparison. In my opinion it's far more intuitive and useful than the js state of affairs. – Troytroyer#[1, 2, 3] === #[1, 2, 3]
. – Pennyworth[1,2,3] == [1,2,3] # returns true
. I guess it just comes down to understanding the nuances of each language. – Sogdiana([] == []) == True
. "In C++, you'd be comparing two pointers - false." You mean in C. Sure, C++ supports C arrays, but in C++std::vector
s are more common and more useful, and(std::vector<T>() == std::vector<T>()) == true
. – Stipendiarystd::array
was not in wide use back then (although it is C++11 thing), and I generally did not considervector
an array because that was kinda reserved for C style arrays. Notably,std::array
also has a==
operator so if you're writing modern C++, my comment is indeed not valid in that sense. But dude... it's 10 years ago. I literally went through 2 jobs, 3 relationships and 3 mortage refinances... – Nazarenestd::vector
to be the C++ equivalent of JavaScript arrays though. They might not have the same name, but the features supported by JavaScript arrays are more similar to those supported bystd::vector
than those supported bystd::array
or C arrays. – Stipendiary