Il modo pratico
Penso che sia sbagliato dire che una particolare implementazione è "The Right Way ™" se è solo "giusta" ("corretta") in contrasto con una soluzione "sbagliata". La soluzione di Tomáš è un netto miglioramento rispetto al confronto di array basato su stringhe, ma ciò non significa che sia oggettivamente "giusta". Cosa è giusto comunque? È il più veloce? È il più flessibile? È il più facile da capire? È il debug più veloce? Utilizza le meno operazioni? Ha effetti collaterali? Nessuna soluzione può avere il meglio di tutte le cose.
Tomáš potrebbe dire che la sua soluzione è veloce, ma direi anche che è inutilmente complicata. Cerca di essere una soluzione all-in-one che funziona per tutti gli array, nidificati o meno. In effetti, accetta anche più di una semplice matrice come input e tenta comunque di fornire una risposta "valida".
I generici offrono riusabilità
La mia risposta affronterà il problema in modo diverso. Inizierò con una arrayCompare
procedura generica che riguarda solo il passaggio attraverso le matrici. Da lì, costruiremo le nostre altre funzioni di confronto di base come arrayEqual
e arrayDeepEqual
, ecc
// 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)
Secondo me, il miglior tipo di codice non ha nemmeno bisogno di commenti, e questa non fa eccezione. Qui sta succedendo così poco che puoi capire il comportamento di questa procedura quasi senza alcuno sforzo. Certo, alcune sintassi di ES6 potrebbero sembrarti estranee adesso, ma questo è solo perché ES6 è relativamente nuovo.
Come suggerisce il tipo, arrayCompare
accetta la funzione di confronto f
e due array di input xs
e ys
. Per la maggior parte, tutto ciò che facciamo è chiamare f (x) (y)
ogni elemento negli array di input. Restituiamo in anticipo false
se i f
resi definiti dall'utente false
- grazie alla &&
valutazione del corto circuito. Quindi sì, questo significa che il comparatore può interrompere l'iterazione in anticipo e impedire il loop attraverso il resto dell'array di input quando non è necessario.
Confronto rigoroso
Successivamente, utilizzando la nostra arrayCompare
funzione, possiamo facilmente creare altre funzioni di cui potremmo avere bisogno. Inizieremo con l'elementare 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
Semplice come quella. arrayEqual
può essere definito con arrayCompare
e una funzione di confronto che si confronta a
con l' b
utilizzo ===
(per una rigorosa uguaglianza).
Si noti che definiamo anche equal
come propria funzione. Ciò evidenzia il ruolo di arrayCompare
una funzione di ordine superiore per utilizzare il nostro comparatore di primo ordine nel contesto di un altro tipo di dati (Array).
Confronto sciolto
Potremmo definire altrettanto facilmente arrayLooseEqual
usando un ==
invece. Ora confrontando 1
(Numero) con '1'
(Stringa), il risultato sarà 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
Confronto profondo (ricorsivo)
Probabilmente avrai notato che questo è solo un confronto superficiale. Sicuramente la soluzione di Tomáš è "The Right Way ™" perché implica un confronto profondo implicito, giusto?
Bene, la nostra arrayCompare
procedura è abbastanza versatile da usare in un modo che rende un test di uguaglianza profonda un gioco da ragazzi ...
// 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
Semplice come quella. Costruiamo un comparatore profondo usando un'altra funzione di ordine superiore. Questa volta stiamo eseguendo il wrapping arrayCompare
utilizzando un comparatore personalizzato che controllerà se a
e b
sono array. In tal caso, riapplica arrayDeepCompare
altrimenti confronta a
e b
al comparatore specificato dall'utente ( f
). Questo ci consente di mantenere il comportamento di confronto profondo separato dal modo in cui confrontiamo effettivamente i singoli elementi. Cioè, come nell'esempio qui sopra, si può confrontare profondo utilizzando equal
, looseEqual
o qualsiasi altro comparatore facciamo.
Perché arrayDeepCompare
è curry, possiamo parzialmente applicarlo come abbiamo fatto anche negli esempi precedenti
// arrayDeepEqual :: [a] -> [a] -> Bool
const arrayDeepEqual =
arrayDeepCompare (equal)
// arrayDeepLooseEqual :: [a] -> [a] -> Bool
const arrayDeepLooseEqual =
arrayDeepCompare (looseEqual)
Per me, questo è già un chiaro miglioramento rispetto alla soluzione di Tomáš perché posso esplicitamente scegliere un confronto superficiale o profondo per i miei array, se necessario.
Confronto oggetti (esempio)
E se avessi una serie di oggetti o qualcosa del genere? Forse vuoi considerare quegli array come "uguali" se ogni oggetto ha lo stesso id
valore ...
// 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
Semplice come quella. Qui ho usato oggetti JS alla vaniglia, ma questo tipo di comparatore potrebbe funzionare per qualsiasi tipo di oggetto; anche i tuoi oggetti personalizzati. La soluzione di Tomáš dovrebbe essere completamente rielaborata per supportare questo tipo di test di uguaglianza
Array profondo con oggetti? Non è un problema. Abbiamo creato funzioni generiche molto versatili, quindi funzioneranno in un'ampia varietà di casi d'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
Confronto arbitrario (esempio)
O se volessi fare qualche altro tipo di confronto completamente arbitrario? Forse voglio sapere se ognuno x
è maggiore di ogni 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
Meno è meglio
Puoi vedere che stiamo effettivamente facendo di più con meno codice. Non c'è nulla di complicato su arrayCompare
se stesso e ciascuno dei comparatori personalizzati che abbiamo realizzato ha un'implementazione molto semplice.
Con facilità, possiamo definire esattamente come desideriamo due matrici per essere confrontati - superficiale, profondo, rigoroso, sciolto, alcune proprietà dell'oggetto, o qualche calcolo arbitrario, o qualsiasi combinazione di questi - tutto utilizzando una procedura , arrayCompare
. Magari anche sognare un RegExp
comparatore! So come i bambini adorano quei regexps ...
È il più veloce? No. Ma probabilmente non è nemmeno necessario. Se la velocità fosse l'unica metrica utilizzata per misurare la qualità del nostro codice, un sacco di codice davvero eccezionale verrebbe buttato via - Ecco perché chiamo questo approccio The Practical Way . O forse per essere più equo, un modo pratico. Questa descrizione è adatta a questa risposta perché non sto dicendo che questa risposta sia pratica solo rispetto ad altre risposte; è oggettivamente vero. Abbiamo raggiunto un alto grado di praticità con pochissimo codice di cui è molto facile ragionare. Nessun altro codice può dire che non abbiamo guadagnato questa descrizione.
Questo la rende la soluzione "giusta" per te? Sta a te decidere. E nessun altro può farlo per te; solo tu sai quali sono le tue esigenze. In quasi tutti i casi, apprezzo il codice semplice, pratico e versatile rispetto a un tipo intelligente e veloce. Ciò che apprezzi potrebbe essere diverso, quindi scegli ciò che funziona per te.
modificare
La mia vecchia risposta era più focalizzata sulla scomposizione arrayEqual
in piccole procedure. È un esercizio interessante, ma non è il modo migliore (più pratico) per affrontare questo problema. Se sei interessato, puoi vedere questa cronologia delle revisioni.