Wierzę, że nauczyłem się niektórych / wielu / większości podstawowych pojęć leżących u podstaw programowania funkcjonalnego w JavaScript. Mam jednak problemy z odczytaniem kodu funkcjonalnego, nawet kodu, który napisałem, i zastanawiam się, czy ktoś może dać mi jakieś wskazówki, porady, najlepsze praktyki, terminologię itp., Które mogą pomóc.
Weź poniższy kod. Napisałem ten kod. Ma na celu przypisanie procentowego podobieństwa między dwoma obiektami, między powiedzmy {a:1, b:2, c:3, d:3}
i {a:1, b:1, e:2, f:2, g:3, h:5}
. Kod powstał w odpowiedzi na to pytanie dotyczące przepełnienia stosu . Ponieważ nie byłem pewien, o jaki procent podobieństwa pytał plakat, podałem cztery różne rodzaje:
- procent kluczy w 1. obiekcie, który można znaleźć w 2.,
- procent wartości w pierwszym obiekcie, który można znaleźć w drugim, w tym duplikaty,
- procent wartości w pierwszym obiekcie, który można znaleźć w drugim, bez dozwolonych duplikatów, oraz
- procent par {klucz: wartość} w 1. obiekcie, który można znaleźć w 2. obiekcie.
Zacząłem od rozsądnego kodu, ale szybko zdałem sobie sprawę, że jest to problem dobrze dostosowany do programowania funkcjonalnego. W szczególności zdałem sobie sprawę, że jeśli uda mi się wyodrębnić funkcję lub trzy dla każdej z czterech powyższych strategii, które określają rodzaj funkcji, którą chciałem porównać (np. Klucze lub wartości itp.), To mogę być w stanie zredukować (ułaskawić grę słów) resztę kodu do powtarzalnych jednostek. Wiesz, utrzymując to na sucho. Więc przeszedłem do programowania funkcjonalnego. Jestem bardzo dumny z wyniku, myślę, że jest dość elegancki i myślę, że rozumiem, co zrobiłem całkiem dobrze.
Jednak nawet po napisaniu kodu i zrozumieniu każdej jego części podczas budowy, kiedy teraz na niego patrzę, nadal jestem nieco zaskoczony zarówno tym, jak czytać poszczególne półtony, jak i jak „grok”, co właściwie robi konkretna półwiersz kodu. Robię mentalne strzały, by łączyć różne części, które szybko rozkładają się w bałagan spaghetti.
Czy ktoś może mi więc powiedzieć, jak „odczytać” bardziej skomplikowane fragmenty kodu w sposób zwięzły i przyczyniający się do zrozumienia tego, co czytam? Sądzę, że części, które mnie najbardziej doceniają, to te, które mają kilka grubych strzałek z rzędu i / lub części, które mają kilka nawiasów z rzędu. Ponownie, u ich podstaw, w końcu mogę zrozumieć logikę, ale (mam nadzieję) istnieje lepszy sposób na szybkie i jasne i bezpośrednie „przyjęcie” szeregu funkcjonalnych programów JavaScript.
Możesz użyć dowolnej linii kodu od dołu, a nawet innych przykładów. Jeśli jednak chcesz ode mnie kilku wstępnych sugestii, oto kilka. Zacznij od dość prostej. Od blisko końca kodu, jest to, że jest przekazywana jako parametr do funkcji: obj => key => obj[key]
. Jak to przeczytać i zrozumieć? Dłuższy przykładem jest jeden pełny funkcji od blisko początku: const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));
. Ostatnia map
część mnie szczególnie.
Należy pamiętać, że w tym momencie ja nie szukają odniesień do Haskell lub symbolicznej abstrakcyjnej notacji lub podstaw zmiękczania itp Co ja jestem szukasz jest zdania po angielsku, że mogę cicho usta patrząc na linię kodu. Jeśli masz referencje, które dokładnie to dotyczą, świetnie, ale nie szukam również odpowiedzi, które mówią, że powinienem przeczytać kilka podstawowych podręczników. Zrobiłem to i dostaję (przynajmniej znaczną część) logikę. Zauważ też, że nie potrzebuję wyczerpujących odpowiedzi (chociaż takie próby byłyby mile widziane): mile widziane byłyby nawet krótkie odpowiedzi, które zapewniają elegancki sposób odczytu pojedynczej linii w innym przypadku kłopotliwego kodu.
Przypuszczam, że część tego pytania brzmi: czy mogę nawet czytać kod funkcjonalny liniowo, od lewej do prawej i od góry do dołu? A może ktoś jest zmuszony stworzyć na stronie kodu obraz przypominający spaghetti, który nie jest liniowy? A jeśli trzeba to zrobić, wciąż musimy przeczytać kod, więc jak wziąć liniowy tekst i połączyć spaghetti?
Wszelkie wskazówki będą mile widziane.
const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };
// x or X is key or value or key/value pair
const getXs = (obj, getX) =>
Object.keys(obj).map(key => getX(obj)(key));
const getPctSameXs = (getX, filter = vals => vals) =>
(objA, objB) =>
filter(getXs(objB, getX))
.reduce(
(numSame, x) =>
getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
0
) / Object.keys(objA).length * 100;
const pctSameKeys = getPctSameXs(obj => key => key);
const pctSameValsDups = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));
console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys: ', pctSameKeys (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups (obj1, obj2));
console.log('% same values, no duplicates: ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps (obj1, obj2));
// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys: 50
// % same values, incl duplicates: 125
// % same values, no duplicates: 75
// % same properties (k/v pairs): 25
źródło
Nie wykonałem dużo wysoce funkcjonalnej pracy w Javascript (co powiedziałbym, że tak jest - większość ludzi mówiących o funkcjonalnym Javascript może używać map, filtrów i redukcji, ale twój kod definiuje własne funkcje wyższego poziomu , które są nieco bardziej zaawansowany), ale zrobiłem to w Haskell i myślę, że przynajmniej część tego doświadczenia tłumaczy. Dam ci kilka wskazówek do rzeczy, których się nauczyłem:
Określenie typów funkcji jest naprawdę ważne. Haskell nie wymaga określenia typu funkcji, ale włączenie typu do definicji znacznie ułatwia czytanie. Chociaż Javascript nie obsługuje jawnego pisania w ten sam sposób, nie ma powodu, aby nie dołączać definicji typu w komentarzu, np .:
Przy odrobinie wprawy w pracy z takimi definicjami typów, wyjaśniają znaczenie funkcji.
Nazewnictwo jest ważne, być może nawet bardziej niż w programowaniu proceduralnym. Wiele programów funkcjonalnych jest napisanych w bardzo zwięzłym stylu, który jest ciężki w konwencji (np. Konwencja, że „xs” jest listą / tablicą i że „x” jest w niej elementem, jest bardzo wszechobecna), ale chyba, że rozumiesz ten styl łatwo zasugerowałbym bardziej pełne nazewnictwo. Patrząc na konkretne nazwy, których używałeś, „getX” jest niejasne, dlatego też „getXs” tak naprawdę niewiele pomaga. Nazwałbym „getXs” czymś w rodzaju „ApplyToProperties”, a „getX” prawdopodobnie byłoby „propertyMapper”. „getPctSameXs” byłoby wówczas „percentPropertiesSameWith” („z”).
Kolejną ważną rzeczą jest napisanie kodu idiomatycznego . Zauważam, że używasz składni
a => b => some-expression-involving-a-and-b
do tworzenia funkcji curry. Jest to interesujące i może być przydatne w niektórych sytuacjach, ale nie robisz tutaj nic, co by korzystało z funkcji curry, i byłoby bardziej idiomatyczne JavaScript, aby zamiast tego używać tradycyjnych funkcji wielu argumentów. Takie postępowanie może ułatwić sprawdzenie, co się dzieje na pierwszy rzut oka. Używasz takżeconst name = lambda-expression
do definiowania funkcji, których użycie byłoby bardziej idiomatycznefunction name (args) { ... }
. Wiem, że są semantycznie nieco różne, ale jeśli nie polegasz na tych różnicach, sugeruję użycie bardziej powszechnego wariantu, jeśli to możliwe.źródło
obj => key => ...
można uprościć,(obj, key) => ...
ponieważ późniejgetX(obj)(key)
można je również uprościćget(obj, key)
. W przeciwieństwie do innej funkcji curry,(getX, filter = vals => vals) => (objA, objB) => ...
nie można jej łatwo uprościć, przynajmniej w kontekście napisanej reszty kodu.