Działanie funkcji reduce

Odpowiedz Nowy wątek
2019-09-02 20:14
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 ??

edytowany 1x, ostatnio: ProzakNo1, 2019-09-02 20:31

Pozostało 580 znaków

2019-09-02 20:47
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))


01010100 01110101 01110100 01100001 01101010 00100000 01101110 01101001 01100101 00100000 01101101 01100001 00100000 01101110 01101001 01100011 00100000 01100011 01101001 01100101 01101011 01100001 01110111 01100101 01100111 01101111 00101110 00100000 01001001 01100011 00100000 01110011 01110100 01101111 01101110 01110100 00101110
edytowany 5x, ostatnio: stivens, 2019-09-02 22:56

Pozostało 580 znaków

2019-09-02 21:03
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.

edytowany 1x, ostatnio: ProzakNo1, 2019-09-02 21:03

Pozostało 580 znaków

2019-09-02 21:06
0

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

Wazna jest tresc lambdy, ktora tam w srodku masz


01010100 01110101 01110100 01100001 01101010 00100000 01101110 01101001 01100101 00100000 01101101 01100001 00100000 01101110 01101001 01100011 00100000 01100011 01101001 01100101 01101011 01100001 01110111 01100101 01100111 01101111 00101110 00100000 01001001 01100011 00100000 01110011 01110100 01101111 01101110 01110100 00101110
edytowany 1x, ostatnio: stivens, 2019-09-02 21:09

Pozostało 580 znaków

2019-09-02 22:42
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[...]e/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.


edytowany 16x, ostatnio: Silv, 2019-09-02 23:54
Pokaż pozostałe 10 komentarzy
Nie rozumiem, o co Ci chodzi. Ale spokojnie, lubię tłumaczyć, jeśli coś rozumiem. ;) (Jeśli nie, to nie staram się nigdy nie tłumaczyć). W pewności, że tak powinny te wszystkie zapisy wyglądać, utwierdza mnie to, że wynik wywołania wyrażenia będącego rozpisaniem wywołania funkcji reduceCompose zgadza się z wynikiem bezpośredniego wywołania funkcji reduceCompose (podanym przez @ProzakNo1). - Silv 2019-09-02 23:09
OP wstawil kod (f,g) => (...args) => (f(g(...args))) ale pewnie sam go nie napisal. "To skad to sie wzielo?" do tego dolozyc Twoj punkt 5, ktory byl pomylony i mi wyszlo ze ergo to ja cos zle mysle :D - stivens 2019-09-02 23:13
Spokojne rozważania zawsze pomagają. :) - Silv 2019-09-02 23:14
PS. I co dwie głowy to nie jedna – jak ja się pomylę, Ty poprawisz, i odwrotnie. - Silv 2019-09-02 23:15
PS2. A mówiąc bardziej nawiasem, to pierwsza lista (pod A jak to działa?) była bardziej chyba pomocą dla mnie, niż okaże się dla @ProzakNo1 (napisałem to jednak trochę w zagmatwany sposób). - Silv 2019-09-02 23:17

Pozostało 580 znaków

2019-09-02 23:09
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 ;)


01010100 01110101 01110100 01100001 01101010 00100000 01101110 01101001 01100101 00100000 01101101 01100001 00100000 01101110 01101001 01100011 00100000 01100011 01101001 01100101 01101011 01100001 01110111 01100101 01100111 01101111 00101110 00100000 01001001 01100011 00100000 01110011 01110100 01101111 01101110 01110100 00101110
edytowany 1x, ostatnio: stivens, 2019-09-03 00:28
Tfu! No właśnie, nie reducer (jak napisałem), tylko reduce bezpośrednio. Słuszna uwaga, poprawię. - Silv 2019-09-02 23:10

Pozostało 580 znaków

2019-09-02 23:17
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


01010100 01110101 01110100 01100001 01101010 00100000 01101110 01101001 01100101 00100000 01101101 01100001 00100000 01101110 01101001 01100011 00100000 01100011 01101001 01100101 01101011 01100001 01110111 01100101 01100111 01101111 00101110 00100000 01001001 01100011 00100000 01110011 01110100 01101111 01101110 01110100 00101110
edytowany 2x, ostatnio: stivens, 2019-09-02 23:23
Pokaż pozostałe 42 komentarze
Spokojnej nocy. Bo reduce jest warta zrozumienia. :) - Silv 2019-09-03 00:46
Dobrze, że są to funkcje strzałkowe, bo gdyby użyć w takiej konstrukcji tradycyjnych funkcji, a w nich this, pewnie z kilka dni by nam zajęło dojście do wyników. - Silv 2019-09-03 00:49
PS. To mi przypomina, że miałem przeczytać standard ECMAScript. - Silv 2019-09-03 00:50
No ja tam folda (aka reduce) rozumialem :D no chyba ze Ty nie? Ale wydawalo mi sie bardziej ze po prostu nie umiesz tego ubrac w slowa - stivens 2019-09-03 00:56
Ja rozumiem, tak napisałem ogólnie; reduce w tym kontekście jest trudna, jak widać. - Silv 2019-09-03 04:27

Pozostało 580 znaków

2019-09-03 00:52
0

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

edytowany 1x, ostatnio: WeiXiao, 2019-09-03 00:53
Silv rozpisał to po swojemu, niechże teraz rozpisze po mojemu! Na pewno ma czas... - Silv 2019-09-03 00:53

Pozostało 580 znaków

2019-09-03 01:25
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.

edytowany 13x, ostatnio: Maciej Cąderek, 2019-09-06 20:01
Świetne! Teraz wiem więcej. Że Ci się chciało. Dziękuję. - Silv 2019-09-03 04:41
Staram się tak ładnie kod pisać jak Ty w tym poście, ale jeszcze mi się nie do końca udaje. ;) - Silv 2019-09-03 04:47
Proszę bardzo, lubię takie pytania. - Maciej Cąderek 2019-09-03 22:13

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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