Практический путь
Я думаю, что неправильно говорить, что конкретная реализация является «Правильным путем», если она только «правильная» («правильная») в отличие от «неправильного» решения. Решение Томаша - это явное улучшение по сравнению со сравнением массивов на основе строк, но это не значит, что оно объективно «правильно». Что правильно в любом случае? Это самый быстрый? Это самый гибкий? Это легче всего понять? Это самый быстрый для отладки? Использует ли он наименьшее количество операций? Есть ли у него побочные эффекты? Ни одно решение не может быть лучшим из всех.
Томаш мог бы сказать, что его решение быстрое, но я бы сказал, что оно слишком сложное. Он пытается быть универсальным решением, которое работает для всех массивов, вложенных или нет. Фактически, он даже принимает в качестве входных данных больше, чем просто массивы, и все еще пытается дать «правильный» ответ.
Дженерики предлагают повторное использование
Мой ответ подойдет к проблеме по-другому. Я начну с общей arrayCompare
процедуры, которая касается только перехода по массивам. Оттуда мы создадим другие наши базовые функции сравнения, такие как arrayEqual
и arrayDeepEqual
т. Д.
// 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)
На мой взгляд, лучший вид кода даже не нуждается в комментариях, и это не исключение. Здесь так мало всего происходит, что вы можете понять поведение этой процедуры практически без усилий. Конечно, некоторые синтаксисы ES6 могут показаться вам чуждыми, но это только потому, что ES6 является относительно новым.
Как предполагает тип, arrayCompare
принимает функцию сравнения f
, и два входных массива, xs
и ys
. По большей части все, что мы делаем, это вызываем f (x) (y)
каждый элемент во входных массивах. Мы возвращаем досрочно, false
если пользовательские f
возвраты false
- благодаря &&
оценке короткого замыкания. Так что да, это означает, что компаратор может остановить итерацию на ранней стадии и предотвратить циклическое прохождение через остальную часть входного массива, когда в этом нет необходимости.
Строгое сравнение
Затем, используя нашу arrayCompare
функцию, мы можем легко создавать другие функции, которые могут нам понадобиться. Начнем с элементарного 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
Просто как тот. arrayEqual
можно определить с arrayCompare
помощью функции сравнения, которая сравнивается a
с b
использованием ===
(для строгого равенства).
Обратите внимание, что мы также определяем equal
как его собственную функцию. Это подчеркивает роль arrayCompare
функции высшего порядка для использования нашего компаратора первого порядка в контексте другого типа данных (Array).
Слабое сравнение
Мы могли бы так же легко определить, arrayLooseEqual
используя ==
вместо этого. Теперь при сравнении 1
(Number) с '1'
(String) результат будет 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
Глубокое сравнение (рекурсивное)
Вы, наверное, заметили, что это только поверхностное сравнение. Конечно, решение Томаша - «Правильный путь ™», потому что оно подразумевает глубокое сравнение, верно?
Что ж, наша arrayCompare
процедура достаточно универсальна, чтобы использовать ее таким образом, чтобы сделать тест на глубокое равенство быстрым…
// 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
Просто как тот. Мы строим глубокий компаратор, используя другую функцию более высокого порядка. На этот раз мы arrayCompare
используем специальный компаратор, который проверяет, являются ли массивы a
и b
есть ли они. Если это так, повторно примените arrayDeepCompare
сравнение a
и b
к указанному пользователем компаратору ( f
). Это позволяет нам отделить поведение глубокого сравнения от того, как мы на самом деле сравниваем отдельные элементы. Т.е., как в примере выше показывает, мы можем глубоко сравнить с помощью equal
, looseEqual
или любой другой компаратор мы делаем.
Поскольку arrayDeepCompare
это карри, мы можем частично применить его так же, как и в предыдущих примерах
// arrayDeepEqual :: [a] -> [a] -> Bool
const arrayDeepEqual =
arrayDeepCompare (equal)
// arrayDeepLooseEqual :: [a] -> [a] -> Bool
const arrayDeepLooseEqual =
arrayDeepCompare (looseEqual)
Для меня это уже явное улучшение по сравнению с решением Томаша, потому что я могу явно выбрать поверхностное или глубокое сравнение для своих массивов, если необходимо.
Сравнение объектов (пример)
А что если у вас есть массив объектов или что-то еще? Может быть, вы хотите считать эти массивы "равными", если каждый объект имеет одинаковое id
значение ...
// 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
Просто как тот. Здесь я использовал ванильные объекты JS, но этот тип компаратора может работать для любого типа объекта; даже ваши пользовательские объекты. Решение Томаша должно быть полностью переработано для поддержки такого теста на равенство
Глубокий массив с объектами? Не проблема. Мы создали универсальные, универсальные функции, поэтому они будут работать в самых разных случаях.
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
Произвольное сравнение (пример)
Или что, если вы хотите провести какое-то совершенно произвольное сравнение? Возможно я хочу знать, больше ли каждый x
чем каждый 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
Меньше - больше
Вы можете видеть, что мы на самом деле делаем больше с меньшим количеством кода. В этом нет ничего сложного arrayCompare
, и каждый из созданных нами пользовательских компараторов имеет очень простую реализацию.
С легкостью, мы можем точно определить , как мы хотим , два массива для сравнения - мелкий, глубокий, строгий, свободный, некоторые свойства объекта, или некоторые произвольные вычисления, или любая комбинация из них - все с помощью одной процедуры , arrayCompare
. Может быть, даже мечтать о RegExp
компараторе! Я знаю, как дети любят эти регулярные выражения ...
Это самый быстрый? Нет. Но это, вероятно, не должно быть либо. Если бы скорость была единственным показателем, используемым для измерения качества нашего кода, было бы выброшено много действительно хорошего кода - вот почему я называю этот подход Практическим путем . Или , может быть более справедливым, Практический подход. Это описание подходит для этого ответа, потому что я не говорю, что этот ответ только практичен по сравнению с некоторым другим ответом; это объективно верно. Мы достигли высокой степени практичности с очень небольшим количеством кода, который очень легко рассуждать. Ни один другой код не может сказать, что мы не заработали это описание.
Это делает это "правильным" решением для вас? Это для вас , чтобы решить. И никто другой не может сделать это для вас; только вы знаете, каковы ваши потребности. Почти во всех случаях я ценю простой, практичный и универсальный код перед умным и быстрым. То, что вы цените, может отличаться, поэтому выберите то, что вам подходит.
редактировать
Мой старый ответ был больше сфокусирован на разложении arrayEqual
на крошечные процедуры. Это интересное упражнение, но не самый лучший (самый практичный) способ решения этой проблемы. Если вам интересно, вы можете увидеть эту историю изменений.