Działanie funkcji reduce

0

Załóżmy, że posiadamy taki kod:

const increment = x => x + 1;
const decrement = x => x - 1;
const double = x => x * 2;

const reduceCompose = (...fns) => fns.reduce(
    (f,g) => (...args) => (f(g(...args)))
);
console.log('Compose ::' + reduceCompose(increment, double, double)(2));

Moje pytanie dotyczy działanie tej funkcji:
Dlaczego działanie zaczyna się od double, double, int, skoro "currentValue" dla pierwszego przebiegu jest increment ??
2 pyt) skad funkcja reduce wie, ze ma pobrac x ??

0

Chyba nie rozumiem pytania ale

  1. Skladanie funkcji. Czyli masz inc(double(double(x)))

  2. Reduce nie pobiera x'a? To zlozylo Ci funkcje i zwrocilo funkcje przyjmujaca x'a ;)

Zamiast (f,g) => (...args) => (f(g(...args)) wydaje mi sie powinno byc (f,g) => (x) => (f(g(x))

0
const data = [10, 20, 30, 40, 50];
const reduceResult = data.reduce((previousValue, currentValue) => {return previousValue + currentValue});

Właśnie nie do końca rozumiem, składanie funkcji, bo:
dla tego przypadku najpierw bierze 10 a nastepnie dodaje 20.
itd.

0

Bo reduce to nie skladanie funkcji. W tym przypadku zostalo tak uzyte jedynie.

Wazna jest tresc lambdy, ktora tam w srodku masz

1

Dlaczego działanie zaczyna się od double, double, int

Jakie działanie?

skad funkcja reduce wie, ze ma pobrac x

Rozumiem, że przez x rozumiesz tę 2 w wywołaniu? Jak @stivens: nie wie, bo bezpośrednio nie pobiera. Wie to dopiero funkcja, która jest zwracana (to jej argument).

A jak to działa?

1. Funkcja reduceCompose przyjmuje jeden argument – tablicę funkcji.
2. Na tej tablicy wywołuje metodę reduce. Przy tym nie ma podanej wartości początkowej, przez co jako wartość początkowa zostaje użyty pierwszy element tablicy, zgodnie ze specyfikacją:
> If no initialValue is supplied, the first element in the array will be used and skipped.
3. Metoda reduce wywołuje na każdej z funkcji funkcję anonimową (...args) => (f(g(...args)) (redukując za jej pomocą tablicę fns).
4. Powyższa funkcja anonimowa przyjmuje jako swój argument tablicę args. Mimochodem mówiąc: zasłania to obiekt args obecny domyślnie dla każdej funkcji. (Mój błąd, obiekt nazywa się arguments i dla funkcji strzałkowych nie jest dostępny).
5. Funkcja reduceCompose zwraca wynik wywołania metody reduce.

Funkcja reduceCompose działa tak:

  1. Przyjmuje dowolną liczbę argumentów, łączonych następnie w jedną tablicę o nazwie fns.
  2. Na tej tablicy wywołuje metodę reduce i zwraca jej wynik. W wywołaniu reduce nie ma podanej wartości początkowej, przez co jako wartość początkowa zostaje użyty pierwszy element tablicy – zgodnie ze specyfikacją:

If no initialValue is supplied, the first element in the array will be used and skipped.

Metoda reduce działa tak:

  1. Przyjmuje jako swój argument funkcję anonimową (f, g) => (...args) => (f(g(...args)) (gdyby ją wydzielić na zewnątrz, można ją nazwać np. reducer).
  2. Dla każdego elementu tablicy fns wywołuje tę funkcję anonimową, redukując tablicę za jej pomocą. Parametr f oznacza akumulator, a parametr g – bieżący element tablicy.
  3. Zwraca wartość akumulatora.

Funkcja anonimowa (f, g) => (...args) => (f(g(...args)) działa tak:

  1. Przyjmuje jako swoje argumenty funkcję f oraz funkcję g.
  2. Zwraca nową funkcję anonimową (...args) => (f(g(...args)).

Dość to zagmatwane, nie spotkałem się z takim użyciem funkcji reduce. Sam bym optował za tym, by nie pisać tak kodu, a jeśli już, to użyć bardziej opisowych nazw. Chyba że kontekst użycia wszystko wyjaśnia (ale nie podałeś go).


Teraz, mając wyrażenie:

reduceCompose(increment, double, double)(2)

to wywołanie funkcji reduceCompose przebiega tak:

  1. Zwraca ona wynik metody reduce dla tablicy [x => x + 1, x => x * 2, x => x * 2].

Wywołanie metody reduce przebiega tak:

  1. Pobiera ona pierwszy element tablicy (czyli x => x + 1) i stosuje go jako akumulator, nie robiąc nic więcej.
  2. Pobiera ona drugi element tablicy (czyli x => x * 2) i podstawia pod wartość akumulatora funkcję anonimową (...args) => (a => a + 1)((b => b * 2)(...args)).
  3. Pobiera ona trzeci element tablicy (czyli x => x * 2) i podstawia pod wartość akumulatora funkcję anonimową (...args1) => ((...args2) => (a => a + 1)((b => b * 2)(...args2)))((c => c * 2)(...args1)).
  4. Kończy działanie i zwraca akumulator.

Następnie zwrócony akumulator (czyli funkcja) jest wywoływany z argumentem 2. Wynik: 9.

Mogłem się gdzieś pomylić w tym opisie, wybacz. Jeśli tak, kto zauważy, niech mnie poprawia.

Ciekawy jestem, czemu ma służyć taka dziwna konstrukcja? Być może czegoś nie widzę, albo się pomyliłem – chętnie się dowiem.


const reduceResult = data.reduce((previousValue, currentValue) => {return previousValue + currentValue});

W zasadzie previousValue powinna nazywać się accumulator, bo nie przechowuje "poprzedniej wartości" w rozumieniu "poprzedniego elementu tablicy", a wynik funkcji metody reduce na dotychczas przetworzonej części tablicy.

Źródło, którego używałem: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce


UPDATE: Poprawiłem błąd przy punkcie nr 4 w pierwszej liście.


UPDATE2: Uwaga do parametru: ...args. Typ (tablica) i nazwa (args) nie są, moim zdaniem, najszczęśliwsze tutaj, ale być może zależą one od kontekstu (którego nie znam). Jak zaznaczył wyżej @stivens, parametr mógłby nazywać się np. x, i byłoby to w tym, wąskim kontekście czytelniejsze.


UPDATE3: Poprawiłem nieścisłość przy punkcie nr 5 w pierwszej liście.


UPDATE4: Poprawiłem kolejną nieścisłość przy punkcie nr 5 w pierwszej liście.


UPDATE5: Usunąłem w ogóle punkt nr 5 z pierwszej listy, bo wprowadzał więcej zamętu niż wyjaśnień. Zaktualizowałem tym samym punkt nr 3 z tej samej listy.


UPDATE6: Poprawiłem pierwszą listę całościowo. Mam nadzieję, teraz będzie więcej zrozumiała.

0

Nie podoba mi sie punkt 5
Powyższa funkcja anonimowa redukuje (reducing) tablicę args (czyli wywołanie kolejnych elementów tablicy, oznaczanych poprzez g), używając akumulatora oznaczonego przez f.

Reduce redukuje tablice na rzecz ktore zostala wywolana ;)

1

Metoda reduce wywołuje na każdej z funkcji funkcję anonimową (...args) => (f(g(...args)).

Funkcja tutaj jest
(f,g) => (...args) => (f(g(...args)))

Roznica jest taka ze prawa strona to zwrocona wartosc.


Funkcja reduce za pomocą powyższej funkcji anonimowej redukuje (reduces) tablicę args (czyli wywołanie kolejnych elementów tablicy, oznaczanych poprzez g), używając akumulatora oznaczonego przez f.

Jak juz to redukuje ...fns

0

A może zapisanie tego iteracyjnie by ułatwiło?

1

@Silv

Nie znam się na składaniu. Co to daje, tak ogólnie?

Odpowiem w poscie, bo więcej miejsca.

Załóżmy, że mamy taka tablicę użytkowników:

const users = [
  { name: 'John', age: 30, gender: 'male' },
  { name: 'Alice', age: 25, gender: 'female' },
  { name: 'Joe', age: 18, gender: 'male' },
]

Zakładam, że wiesz co to jest chainowanie metod w JSie i jak wygodne to jest. Stwórzmy sobie nową tablicę z wiekiem mężczyzn:

const isMale = (user) => user.gender === 'male'
const getAge = (user) => user.age

const malesAge = users
  .filter(isMale)
  .map(getAge)

Ok, fajnie - wygląda elegancko. Załózmy teraz, że dodatkowo chcemy stworzyć funkcję, która znajdzie wiek najstarszego mężczyzny, zserializuje dane i zapisze je do pliku, mogłoby to wyglądac tak:

const serialize = (name, value) => {
  return JSON.stringify({ name, value })
}

const fakeSave = (file, content) => {
  console.log(`Saved as ${file} with content: ${content}`)
}

const isMale = (user) => user.gender === 'male'
const getAge = (user) => user.age

const saveOldestMaleAge = (users) => {
  const malesAge = users
    .filter(isMale)
    .map(getAge)

  const oldestMaleAge = Math.max(...malesAge)
  const serialized = serialize('oldestMaleAge', oldestMaleAge)

  fakeSave('example.txt', serialized)
}

saveOldestMaleAge(users)

Nie ma tragedii, ale rozwiązanie ma kilka minusów:

  • nie można chainować funkcji jeśli nie jest metodą obiektu na którym operujemy - mamy mix chainowania i zwykłych wywołań funkcji,
  • tworzymy trzy dodatkowe zmienne, dla których trzeba wymyślić nazwy,
  • recznie musimy przepychać wynik poprzedniej funkcji do funkcji następnej.

Oczywiście możemy pominąć dwa ostatnie punkty, ale robi się wtedy nieczytelna, zagnieżdżona kupa:

const serialize = (name, value) => {
  return JSON.stringify({ name, value })
}

const fakeSave = (file, content) => {
  console.log(`Saved as ${file} with content: ${content}`)
}

const isMale = (user) => user.gender === 'male'
const getAge = (user) => user.age

const saveOldestMaleAge = (users) => {
  fakeSave(
    'example.txt',
    serialize(
      'oldestMaleAge',
      Math.max(
        ...users
          .filter(isMale)
          .map(getAge)
      )
    ),
  )
}

saveOldestMaleAge(users)

Stosując compose (lub w moim przypadku pipe, które działa niemal identycznie tylko ma imo bardziej naturalną kolejność wywołań), możemy zrobić to bardziej elegancko:

const pipe = (...fns) => (initialArg) => {
  return fns.reduce((arg, fn) => fn(arg), initialArg)
}

const filter = (fn) => (arr) => arr.filter(fn)
const map = (fn) => (arr) => arr.map(fn)
const isMale = (user) => user.gender === 'male'
const getAge = (user) => user.age
const getMax = (arr) => Math.max(...arr)

const serialize = (name) => (value) => {
  return JSON.stringify({ name, value })
}

const fakeSave = (file) => (content) => {
  console.log(`Saved as ${file} with content: ${content}`)
}

const saveOldestMaleAge = pipe(
  filter(isMale),
  map(getAge),
  getMax,
  serialize('oldestMaleAge'),
  fakeSave('example.txt'),
)

saveOldestMaleAge(users)

Minusem jest to, że JS, w przeciwieństwie do języków functional-first - ma mało funkcyjną bibliotekę standardową, więc by móc w ten sposób skorzystać z Array.prototye.filter, Array.prototype.map i Math.max trzeba napisać proste wrapery, ale jest to czynność jednorazowa i banalna (można też użyć gotowych bibliotek, które maja mnóstwo funkcji gotowych do takiego łączenia, np: ramda, lodash/fp).

Niewątpliwą zaletą jest natomiast to, że główna funkcja saveOldestMaleAge nie potrzebuje pośrednich zmiennych i automatycznie przekazuje wyniki z jednej podfunkcji do drugiej, przy zachowaniu czytelności (czyta się wręcz jak pseudokod z krokami algorytmu). Takie podejście jest też maksymalnie reużywalne i komponowalne.

Można powiedzieć, że pipe i compose to taki funkcyjny odpowiednik chainowania, tyle, że nie jest ograniczony do metod obiektu, ale może uzywać dowolnych funkcji o zgodnym typie.

CodePen: https://codepen.io/caderek/pen/RwbLZMO?editors=0012


PS
Wersja compose, której ja używam (imo prostsza do zrozumienia niż wersja OP), analogicznie do pipe:

const compose = (...fns) => (initialArg) => {
  return fns.reduceRight((arg, fn) => fn(arg), initialArg)
}

Jest tu jedna róznica w API w stosunku do wersji OP - w wersji OP pierwsza funkcja może przyjąć więcej niż jeden argument, co nie jest konieczne jak masz currying.

PPS
W rzeczywistym przykładzie pewnie podzieliłby odpowiedzialności funkcji saveOldestMaleAge, ale jako przykład komponowania funkcji myślę, że ujdzie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1