O Caminho Prático
Eu acho errado dizer que uma implementação específica é "The Right Way ™" se for apenas "certa" ("correta") em contraste com uma solução "errada". A solução de Tomáš é uma clara melhoria na comparação de matrizes baseada em strings, mas isso não significa que seja objetivamente "correto". O que é certo, afinal? É o mais rápido? É o mais flexível? É o mais fácil de entender? É o mais rápido para depurar? Utiliza menos operações? Isso tem algum efeito colateral? Nenhuma solução pode ter o melhor de todas as coisas.
Tomás poderia dizer que sua solução é rápida, mas eu também diria que é desnecessariamente complicado. Ele tenta ser uma solução completa que funciona para todas as matrizes, aninhadas ou não. De fato, ele aceita mais do que apenas matrizes como entrada e ainda tenta dar uma resposta "válida".
Os genéricos oferecem reutilização
Minha resposta abordará o problema de maneira diferente. Começarei com um arrayCompare
procedimento genérico que se preocupa apenas em percorrer as matrizes. A partir daí, criaremos nossas outras funções básicas de comparação, como arrayEqual
e 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)
Na minha opinião, o melhor tipo de código nem precisa de comentários, e isso não é exceção. Há tão pouco acontecendo aqui que você pode entender o comportamento deste procedimento quase sem nenhum esforço. Claro, algumas das sintaxes do ES6 podem parecer estranhas para você agora, mas isso ocorre apenas porque o ES6 é relativamente novo.
Como o tipo sugere, arrayCompare
assume a função de comparação,, f
e duas matrizes de entrada, xs
e ys
. Na maior parte, tudo o que fazemos é chamar f (x) (y)
cada elemento nas matrizes de entrada. Retornamos cedo false
se o f
retorno definido pelo usuário false
- graças à &&
avaliação de curto-circuito. Portanto, sim, isso significa que o comparador pode interromper a iteração antecipadamente e impedir o loop pelo restante da matriz de entrada quando desnecessário.
Comparação estrita
Em seguida, usando nossa arrayCompare
função, podemos criar facilmente outras funções que possamos precisar. Vamos começar com o elementar 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
Simples assim. arrayEqual
pode ser definido com arrayCompare
e uma função comparadora que se compara a
ao b
uso===
(para igualdade estrita).
Observe que também definimos equal
como sua própria função. Isso destaca o papel de arrayCompare
uma função de ordem superior para utilizar nosso comparador de primeira ordem no contexto de outro tipo de dados (Matriz).
Comparação fraca
Poderíamos ser facilmente definidos arrayLooseEqual
usando um ==
. Agora, ao comparar 1
(Number) a '1'
(String), o resultado será 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
Comparação profunda (recursiva)
Você provavelmente já reparou que essa é apenas uma comparação superficial. Certamente a solução de Tomáš é "The Right Way ™" porque implica uma comparação profunda implícita, certo?
Bem, nosso arrayCompare
procedimento é versátil o suficiente para ser usado de uma maneira que torna fácil um teste de igualdade profundo…
// 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
Simples assim. Construímos um comparador profundo usando outra função de ordem superior. Desta vez, estamos encerrando arrayCompare
usando um comparador personalizado que verificará se a
e b
são matrizes. Nesse arrayDeepCompare
caso , aplique novamente a comparação a
e b
o comparador especificado pelo usuário ( f
). Isso nos permite manter o profundo comportamento de comparação separado de como realmente comparamos os elementos individuais. Ou seja, como mostra o exemplo acima, podemos comparar profundamente usando equal
,looseEqual
ou qualquer outra comparação que fazemos.
Por arrayDeepCompare
ser curry, podemos aplicá-lo parcialmente como fizemos nos exemplos anteriores
// arrayDeepEqual :: [a] -> [a] -> Bool
const arrayDeepEqual =
arrayDeepCompare (equal)
// arrayDeepLooseEqual :: [a] -> [a] -> Bool
const arrayDeepLooseEqual =
arrayDeepCompare (looseEqual)
Para mim, isso já é uma clara melhoria em relação à solução de Tomáš, porque eu posso escolher explicitamente uma comparação superficial ou profunda para minhas matrizes, conforme necessário.
Comparação de objetos (exemplo)
Agora, e se você tiver uma variedade de objetos ou algo assim? Talvez você queira considerar essas matrizes como "iguais" se cada objeto tiver o mesmo id
valor ...
// 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
Simples assim. Aqui eu usei objetos JS vanilla, mas esse tipo de comparador pode funcionar para qualquer tipo de objeto; até seus objetos personalizados. A solução de Tomáš precisaria ser completamente reformulada para suportar esse tipo de teste de igualdade
Matriz profunda com objetos? Não é um problema. Criamos funções genéricas altamente versáteis, para que funcionem em uma ampla variedade de casos de uso.
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
Comparação arbitrária (exemplo)
Ou, e se você quisesse fazer algum outro tipo de comparação completamente arbitrária? Talvez eu queira saber se cada um x
é maior que cada um 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
Menos é mais
Você pode ver que estamos realmente fazendo mais com menos código. Não há nada de complicado em arrayCompare
si e cada um dos comparadores personalizados que criamos tem uma implementação muito simples.
Com facilidade, podemos definir exatamente como nós desejamos para duas matrizes para ser comparado - superficial, profunda, rigorosa, solto, alguma propriedade objeto, ou alguma computação arbitrária, ou qualquer combinação destes - todos usando um único procedimento , arrayCompare
. Talvez até sonhe com um RegExp
comparador! Eu sei como as crianças adoram esses regexps ...
É o mais rápido? Não. Mas provavelmente também não precisa ser. Se a velocidade for a única métrica usada para medir a qualidade do nosso código, muitos códigos realmente ótimos serão descartados. É por isso que estou chamando essa abordagem de The Practical Way . Ou talvez para ser mais justo, uma maneira prática. Esta descrição é adequada para esta resposta porque não estou dizendo que esta resposta é apenas prática em comparação com alguma outra resposta; é objetivamente verdadeiro. Atingimos um alto grau de praticidade com muito pouco código e muito fácil de raciocinar. Nenhum outro código pode dizer que não recebemos essa descrição.
Isso faz com que seja a solução "certa" para você? Isso depende de você decidir. E ninguém mais pode fazer isso por você; só você sabe quais são suas necessidades. Em quase todos os casos, valorizo códigos simples, práticos e versáteis, em vez de tipos inteligentes e rápidos. O que você valoriza pode ser diferente, então escolha o que funciona melhor para você.
Editar
Minha resposta antiga estava mais focada em decompor-se arrayEqual
em procedimentos minúsculos. É um exercício interessante, mas não é realmente a melhor (mais prática) maneira de abordar esse problema. Se você estiver interessado, poderá ver este histórico de revisões.