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 arrayCompare
procedury, która dotyczy tylko przechodzenia przez tablice. Następnie zbudujemy nasze inne podstawowe funkcje porównania, takie jak arrayEqual
i arrayDeepEqual
itp
// 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, arrayCompare
przyjmuje funkcję porównania f
oraz dwie tablice wejściowe xs
i ys
. W większości przypadków wywołujemy f (x) (y)
każdy element w tablicach wejściowych. Wracamy wcześnie, false
jeśli f
zwrot 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 arrayCompare
funkcji, 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. arrayEqual
można zdefiniować za arrayCompare
pomocą funkcji porównawczej, która porównuje się a
do b
użycia ===
(dla ścisłej równości).
Zauważ, że my również definiujemy equal
jako własną funkcję. Podkreśla to rolę arrayCompare
funkcji 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ć arrayLooseEqual
za 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 arrayCompare
procedura 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 arrayCompare
przy użyciu niestandardowych porównanie, że sprawdzi, czy a
i b
są tablice. Jeśli tak, zastosuj ponownie w arrayDeepCompare
innym przypadku a
i b
do 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
, looseEqual
lub dowolny inny komparator robimy.
Ponieważ arrayDeepCompare
jest 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ą id
wartość…
// 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 x
jest 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 RegExp
komparator! 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 arrayEqual
na 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.