Praktyczny sposób
Myślę, że błędem jest twierdzenie, że konkretna implementacja to „Właściwy sposób”, jeśli jest tylko „właściwa” („poprawna”) w przeciwieństwie do „złego” rozwiązania. Rozwiązanie Tomáša stanowi wyraźną poprawę w porównaniu z porównywaniem tablic łańcuchowych, ale to nie znaczy, że jest obiektywnie „właściwe”. Co właściwie jest właściwe ? Czy to jest najszybsze? Czy to jest najbardziej elastyczne? Czy najłatwiej to zrozumieć? Czy debugowanie jest najszybsze? Czy używa najmniej operacji? Czy ma jakieś skutki uboczne? Żadne rozwiązanie nie może mieć wszystkiego najlepszego.
Tomáš mógł powiedzieć, że jego rozwiązanie jest szybkie, ale powiedziałbym również, że jest to niepotrzebnie skomplikowane. Stara się być rozwiązaniem typu „wszystko w jednym”, które działa dla wszystkich tablic, zagnieżdżonych lub nie. W rzeczywistości przyjmuje nawet więcej niż tylko tablice jako dane wejściowe i nadal próbuje udzielić „prawidłowej” odpowiedzi.
Produkty generyczne oferują możliwość ponownego użycia
Moja odpowiedź podejdzie do problemu inaczej. Zacznę od ogólnej arrayCompareprocedury, która dotyczy tylko przechodzenia przez tablice. Następnie zbudujemy nasze inne podstawowe funkcje porównania, takie jak arrayEquali arrayDeepEqualitp
// 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)
Moim zdaniem najlepszy rodzaj kodu nie wymaga nawet komentarzy i nie jest to wyjątkiem. Tutaj dzieje się tak mało, że można zrozumieć zachowanie tej procedury bez żadnego wysiłku. Pewnie, niektóre ze składni ES6 mogą wydawać się wam teraz obce, ale tylko dlatego, że ES6 jest stosunkowo nowy.
Jak sugeruje typ, arrayCompareprzyjmuje funkcję porównania foraz dwie tablice wejściowe xsi ys. W większości przypadków wywołujemy f (x) (y)każdy element w tablicach wejściowych. Wracamy wcześnie, falsejeśli fzwrot zdefiniowany przez użytkownika false- dzięki &&ocenie zwarcia. Tak, oznacza to, że komparator może wcześniej przerwać iterację i zapobiec zapętlaniu się przez resztę tablicy wejściowej, gdy nie jest to konieczne.
Ścisłe porównanie
Następnie, korzystając z naszej arrayComparefunkcji, możemy łatwo stworzyć inne funkcje, których możemy potrzebować. Zaczniemy od elementarnego 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
Proste. arrayEqualmożna zdefiniować za arrayComparepomocą funkcji porównawczej, która porównuje się ado bużycia ===(dla ścisłej równości).
Zauważ, że my również definiujemy equaljako własną funkcję. Podkreśla to rolę arrayComparefunkcji wyższego rzędu w wykorzystaniu naszego komparatora pierwszego rzędu w kontekście innego typu danych (Array).
Luźne porównanie
Możemy równie łatwo zdefiniować arrayLooseEqualza pomocą ==zamiast. Teraz podczas porównywania 1(Liczba) do '1'(Ciąg) wynikiem będzie 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
Głębokie porównanie (rekurencyjne)
Prawdopodobnie zauważyłeś, że jest to tylko płytkie porównanie. Z pewnością rozwiązaniem Tomáša jest „The Right Way ™”, ponieważ zawiera niejawne głębokie porównanie, prawda?
Nasza arrayCompareprocedura jest na tyle wszechstronna, że można ją stosować w taki sposób, że test głębokiej równości jest dziecinnie prosty…
// 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
Proste. Budujemy głęboki komparator za pomocą innej funkcji wyższego rzędu. Tym razem mamy do pakowania arrayCompareprzy użyciu niestandardowych porównanie, że sprawdzi, czy ai bsą tablice. Jeśli tak, zastosuj ponownie w arrayDeepCompareinnym przypadku ai bdo komparatora określonego przez użytkownika ( f). To pozwala nam oddzielić zachowanie do głębokiego porównania od tego, jak faktycznie porównujemy poszczególne elementy. Czyli jak pokazuje powyższy przykład, możemy porównać stosując głęboko equal, looseEquallub dowolny inny komparator robimy.
Ponieważ arrayDeepComparejest curry, możemy go częściowo zastosować, tak jak w poprzednich przykładach
// arrayDeepEqual :: [a] -> [a] -> Bool
const arrayDeepEqual =
arrayDeepCompare (equal)
// arrayDeepLooseEqual :: [a] -> [a] -> Bool
const arrayDeepLooseEqual =
arrayDeepCompare (looseEqual)
Dla mnie jest to już wyraźna poprawa w stosunku do rozwiązania Tomáša, ponieważ w razie potrzeby mogę wyraźnie wybrać płytkie lub głębokie porównanie moich zestawów.
Porównanie obiektów (przykład)
Co teraz, jeśli masz tablicę obiektów lub coś takiego? Może chcesz uznać te tablice za „równe”, jeśli każdy obiekt ma tę samą idwartość…
// 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
Proste. Tutaj użyłem waniliowych obiektów JS, ale ten typ komparatora może działać dla dowolnego typu obiektu; nawet twoje niestandardowe obiekty. Rozwiązanie Tomáša musiałoby zostać całkowicie przerobione, aby obsługiwać ten rodzaj testu równości
Głęboka tablica z obiektami? Żaden problem. Zbudowaliśmy bardzo wszechstronne, ogólne funkcje, dzięki czemu będą działać w szerokim zakresie zastosowań.
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
Arbitralne porównanie (przykład)
A co, jeśli chcesz dokonać innego rodzaju całkowicie arbitralnego porównania? Może chcę wiedzieć, czy każdy xjest większy niż każdy 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
Mniej znaczy więcej
Widać, że faktycznie robimy więcej przy mniejszym kodzie. Nie ma w sobie nic skomplikowanego arrayCompare, a każdy stworzony przez nas niestandardowy komparator ma bardzo prostą implementację.
Z łatwością możemy dokładnie określić, w jaki sposób chcemy na dwie tablice należy porównać - płytkie, głębokie, ścisłe, luźne, niektóre właściwości obiektu, lub dowolna część obliczeń, lub dowolna kombinacja tych form - wszystko za pomocą jednej procedury , arrayCompare. Może nawet wymarzysz RegExpkomparator! Wiem, jak dzieci uwielbiają te wyrażenia regularne…
Czy to jest najszybsze? Nie. Ale prawdopodobnie nie musi tak być. Jeśli szybkość jest jedyną miarą używaną do pomiaru jakości naszego kodu, wiele naprawdę świetnych kodów zostałoby wyrzuconych - dlatego nazywam to podejście Praktycznym . Lub może być bardziej sprawiedliwy, praktyczny sposób. Ten opis jest odpowiedni dla tej odpowiedzi, ponieważ nie twierdzę, że ta odpowiedź jest praktyczna w porównaniu z innymi odpowiedziami; jest to obiektywnie prawdziwe. Osiągnęliśmy wysoki stopień praktyczności przy bardzo małym kodzie, o którym bardzo łatwo jest myśleć. Żaden inny kod nie może powiedzieć, że nie zasłużyliśmy na ten opis.
Czy to sprawia, że jest to „właściwe” rozwiązanie dla Ciebie? To zależy od ciebie . I nikt inny nie może tego dla ciebie zrobić; tylko ty wiesz, jakie są twoje potrzeby. W prawie wszystkich przypadkach cenię prosty, praktyczny i wszechstronny kod nad sprytnym i szybkim rodzajem. To, co cenisz, może się różnić, więc wybierz to, co Ci odpowiada.
Edytować
Moja stara odpowiedź bardziej koncentrowała się na rozkładaniu arrayEqualna małe procedury. To ciekawe ćwiczenie, ale nie jest to najlepszy (najbardziej praktyczny) sposób podejścia do tego problemu. Jeśli jesteś zainteresowany, możesz zobaczyć tę historię zmian.