JavaScript Fungsional: lima cara untuk menemukan rata-rata aritmatika elemen array dan metode .reduce ()

Metode iterasi array mirip dengan "obat permulaan" (tentu saja, itu bukan narkoba; dan saya tidak mengatakan bahwa obat itu baik; mereka hanya kiasan). Karena mereka, banyak "duduk" pada pemrograman fungsional. Masalahnya adalah mereka sangat nyaman. Selain itu, sebagian besar metode ini sangat mudah dipahami. Metode seperti .map() dan .filter() menerima hanya satu argumen panggilan balik dan memungkinkan Anda untuk memecahkan masalah sederhana. Tetapi ada perasaan bahwa metode .reduce() menyebabkan beberapa kesulitan bagi banyak orang. Untuk memahaminya sedikit lebih sulit.



Saya sudah menulis tentang mengapa saya pikir .reduce() menciptakan banyak masalah. Ini sebagian disebabkan oleh kenyataan bahwa banyak manual menunjukkan penggunaan .reduce() hanya ketika menangani angka. Oleh karena itu, saya menulis tentang berapa banyak tugas yang tidak menyiratkan operasi aritmatika dapat diselesaikan menggunakan .reduce() . Tetapi bagaimana jika Anda benar-benar perlu bekerja dengan angka?

Penggunaan khas .reduce() tampak seperti perhitungan rata-rata aritmatika dari elemen-elemen array. Sekilas, tampaknya tidak ada yang istimewa dalam tugas ini. Tapi dia tidak sesederhana itu. Faktanya adalah bahwa sebelum menghitung rata-rata, Anda perlu menemukan indikator berikut:

  1. Jumlah total nilai elemen array.
  2. Panjang array.

Mengetahui semua ini cukup sederhana. Dan perhitungan nilai rata-rata untuk array numerik juga bukan operasi yang sulit. Ini adalah contoh dasar:

 function average(nums) {    return nums.reduce((a, b) => (a + b)) / nums.length; } 

Seperti yang Anda lihat, tidak ada ketidakpahaman khusus di sini. Tetapi tugas menjadi lebih sulit jika Anda harus bekerja dengan struktur data yang lebih kompleks. Bagaimana jika kita memiliki berbagai objek? Bagaimana jika beberapa objek dari array ini perlu disaring? Apa yang harus dilakukan jika Anda perlu mengekstraksi nilai numerik tertentu dari objek? Dalam situasi ini, menghitung nilai rata-rata untuk elemen array sudah tugas yang sedikit lebih rumit.

Untuk mengatasi ini, kami akan menyelesaikan masalah pelatihan (ini didasarkan pada tugas ini dengan FreeCodeCamp). Kami akan menyelesaikannya dengan lima cara berbeda. Masing-masing dari mereka memiliki kelebihan dan kekurangan. Analisis dari lima pendekatan ini untuk menyelesaikan masalah ini akan menunjukkan seberapa fleksibelnya JavaScript. Dan saya berharap bahwa analisis solusi akan memberi Anda makanan untuk dipikirkan tentang cara menggunakan .reduce() dalam proyek nyata.

Ikhtisar Tugas


Misalkan kita memiliki berbagai objek yang menggambarkan ekspresi bahasa gaul Victoria. Anda perlu memfilter ekspresi yang tidak ditemukan di Google Buku (properti yang found dari objek yang sesuai adalah false ), dan menemukan peringkat rata-rata untuk popularitas ekspresi. Seperti apa data tersebut nantinya (diambil dari sini ):

 const victorianSlang = [           term: 'doing the bear',        found: true,        popularity: 108,    },           term: 'katterzem',        found: false,        popularity: null,    },           term: 'bone shaker',        found: true,        popularity: 609,    },           term: 'smothering a parrot',        found: false,        popularity: null,    },           term: 'damfino',        found: true,        popularity: 232,    },           term: 'rain napper',        found: false,        popularity: null,    },           term: 'donkey's breakfast',        found: true,        popularity: 787,    },           term: 'rational costume',        found: true,        popularity: 513,    },           term: 'mind the grease',        found: true,        popularity: 154,    }, ]; 

Pertimbangkan 5 cara untuk menemukan nilai rata-rata estimasi popularitas ekspresi dari array ini.

1. Memecahkan masalah tanpa menggunakan .reduce () (loop wajib)


Dalam pendekatan pertama kami untuk memecahkan masalah, metode .reduce() tidak akan digunakan. Jika Anda belum menemukan metode untuk mengulangi array sebelumnya, maka saya berharap bahwa menguraikan contoh ini akan sedikit memperjelas situasinya untuk Anda.

 let popularitySum = 0; let itemsFound = 0; const len = victorianSlang.length; let item = null; for (let i = 0; i < len; i++) {    item = victorianSlang[i];    if (item.found) {        popularitySum = item.popularity + popularitySum;        itemsFound = itemsFound + 1;   } const averagePopularity = popularitySum / itemsFound; console.log("Average popularity:", averagePopularity); 

Jika Anda terbiasa dengan JavaScript, maka Anda akan dengan mudah memahami contoh ini. Faktanya, hal berikut terjadi di sini:

  1. Kami menginisialisasi variabel popularitySum dan itemsFound . Variabel pertama, popularitySum , menyimpan peringkat keseluruhan popularitas ekspresi. Dan variabel kedua, itemsFound , (itu kejutan) menyimpan jumlah ekspresi yang ditemukan.
  2. Kemudian kita menginisialisasi len konstan dan item variabel, yang berguna bagi kita ketika melintasi array.
  3. Dalam for , counter i bertambah sampai nilainya mencapai nilai indeks elemen terakhir dari array.
  4. Di dalam loop, kita mengambil elemen array yang ingin kita jelajahi. Kami mengakses elemen menggunakan konstruk victorianSlang[i] .
  5. Kemudian kami mencari tahu apakah ungkapan ini ditemukan dalam koleksi buku.
  6. Jika sebuah ekspresi muncul di buku, kami mengambil nilai peringkat popularitasnya dan menambah nilai dari variabel popularitySum .
  7. Pada saat yang sama, kami juga meningkatkan counter dari ekspresi yang ditemukan - itemsFound .
  8. Dan akhirnya, kami menemukan rata-rata dengan membagi popularitySum per itemsFound .

Jadi, kami mengatasi tugas itu. Mungkin keputusan kami tidak terlalu indah, tetapi melakukan tugasnya. Menggunakan metode untuk beralih melalui array akan membuatnya sedikit lebih bersih. Mari kita lihat apakah kita berhasil, dan kebenarannya adalah "membersihkan" keputusan ini.

2. Solusi sederhana # 1: .filter (), .map () dan menemukan jumlah menggunakan .reduce ()


Mari, sebelum upaya pertama untuk menggunakan metode array untuk menyelesaikan masalah, kami memecahnya menjadi bagian-bagian kecil. Yaitu, inilah yang perlu kita lakukan:

  1. Pilih objek yang mewakili ekspresi yang ada di koleksi Google Buku. Di sini Anda dapat menggunakan metode .filter() .
  2. Ekstrak dari objek evaluasi popularitas ekspresi. Untuk mengatasi subtugas ini, metode .map() cocok.
  3. Hitung jumlah peringkat. Di sini kita dapat menggunakan bantuan teman lama kita .reduce() .
  4. Dan akhirnya, temukan nilai rata-rata estimasi.

Begini tampilannya dalam kode:

 //   // ---------------------------------------------------------------------------- function isFound(item) {    return item.found; }; function getPopularity(item) {    return item.popularity; } function addScores(runningTotal, popularity) {    return runningTotal + popularity; } //  // ---------------------------------------------------------------------------- //  ,      . const foundSlangTerms = victorianSlang.filter(isFound); //   ,   . const popularityScores = foundSlangTerms.map(getPopularity); //     .    ,    //   ,  reduce     ,  0. const scoresTotal = popularityScores.reduce(addScores, 0); //       . const averagePopularity = scoresTotal / popularityScores.length; console.log("Average popularity:", averagePopularity); 

addScore fungsi addScore , dan baris tempat .reduce() dipanggil. Perhatikan bahwa addScore menerima dua parameter. Yang pertama, runningTotal , dikenal sebagai baterai. Ini menyimpan jumlah nilai. Nilainya berubah setiap kali ketika kami mengulangi array dan menjalankan return . Parameter kedua, popularity , adalah elemen terpisah dari array yang sedang kami proses. Pada awal addScore atas array, return addScore di addScore belum pernah dieksekusi. Ini berarti runningTotal belum diatur secara otomatis. Oleh karena itu, dengan memanggil .reduce() , kami meneruskan ke metode ini nilai yang perlu ditulis untuk runningTotal di awal. Ini adalah parameter kedua yang dilewatkan ke .reduce() .

Jadi, kami menerapkan metode iterating array untuk menyelesaikan masalah. Versi baru dari solusi ini ternyata jauh lebih bersih daripada yang sebelumnya. Dengan kata lain, keputusan itu ternyata lebih bersifat deklaratif. Kami tidak memberi tahu JavaScript tentang cara mengeksekusi loop secara tepat; kami tidak mengikuti indeks elemen-elemen array. Sebagai gantinya, kami mendeklarasikan fungsi pembantu sederhana berukuran kecil dan menggabungkannya. Semua kerja keras dilakukan untuk kita dengan metode array .filter() , .map() dan .reduce() . Pendekatan untuk memecahkan masalah seperti ini lebih ekspresif. Metode array ini jauh lebih lengkap daripada yang bisa dilakukan loop, mereka memberi tahu kami tentang maksud yang ditetapkan dalam kode.

3. Solusi Mudah # 2: Menggunakan Banyak Baterai


Dalam versi sebelumnya dari solusi, kami menciptakan sejumlah variabel menengah. Misalnya, foundSlangTerms dan foundSlangTerms . Dalam kasus kami, solusi semacam itu cukup dapat diterima. Tetapi bagaimana jika kita menetapkan tujuan yang lebih kompleks tentang desain kode? Alangkah baiknya jika kita bisa menggunakan pola desain antarmuka yang lancar dalam program ini. Dengan pendekatan ini, kita akan dapat mengaitkan panggilan semua fungsi dan dapat melakukannya tanpa variabel perantara. Namun, satu masalah menunggu kita di sini. Perhatikan bahwa kita perlu mendapatkan nilai popularityScores.length . Jika kita akan mengaitkan semuanya, maka kita perlu cara lain untuk menemukan jumlah elemen dalam array. Jumlah elemen dalam array memainkan peran pembagi dalam menghitung nilai rata-rata. Mari kita lihat apakah kita bisa mengubah pendekatan untuk memecahkan masalah sehingga semuanya bisa dilakukan dengan menggabungkan pemanggilan metode dalam sebuah rantai. Kami akan melakukan ini dengan melacak dua nilai ketika mengulangi elemen array, yaitu menggunakan "baterai ganda".

 //   // --------------------------------------------------------------------------------- function isFound(item) {    return item.found; }; function getPopularity(item) {    return item.popularity; } //    ,  return,   . function addScores({totalPopularity, itemCount}, popularity) {    return {        totalPopularity: totalPopularity + popularity,        itemCount:    itemCount + 1,    }; } //  // --------------------------------------------------------------------------------- const initialInfo  = {totalPopularity: 0, itemCount: 0}; const popularityInfo = victorianSlang.filter(isFound)    .map(getPopularity)    .reduce(addScores, initialInfo); //       . const {totalPopularity, itemCount} = popularityInfo; const averagePopularity = totalPopularity / itemCount; console.log("Average popularity:", averagePopularity); 

Di sini, untuk bekerja dengan dua nilai, kami menggunakan objek dalam fungsi peredam. Setiap melewati array yang dilakukan menggunakan addScrores , kami memperbarui nilai total peringkat popularitas dan jumlah elemen. Penting untuk dicatat bahwa kedua nilai ini direpresentasikan sebagai objek tunggal. Dengan pendekatan ini, kita dapat "menipu" sistem dan menyimpan dua entitas dalam nilai pengembalian yang sama.

Fungsi addScrores sedikit lebih rumit daripada fungsi dengan nama yang sama pada contoh sebelumnya. Tapi sekarang ternyata kita bisa menggunakan satu rantai pemanggilan metode untuk melakukan semua operasi dengan array. Sebagai hasil dari pemrosesan array, kami mendapatkan objek popularInfo, yang menyimpan semua yang Anda butuhkan untuk menemukan rata-rata. Ini membuat rantai panggilan rapi dan sederhana.

Jika Anda merasakan keinginan untuk meningkatkan kode ini, maka Anda dapat mencobanya. Misalnya - Anda dapat mengulanginya untuk menghilangkan banyak variabel perantara. Anda bahkan dapat mencoba meletakkan kode ini pada satu baris.

4. Komposisi fungsi tanpa menggunakan notasi titik


Jika Anda baru mengenal pemrograman fungsional, atau jika Anda merasa pemrograman fungsional terlalu rumit, Anda dapat melewati bagian ini. Mem-parsing akan menguntungkan Anda jika Anda sudah terbiasa dengan curry() dan compose() . Jika Anda ingin mempelajari topik ini, lihat materi ini tentang pemrograman fungsional dalam JavaScript, dan, khususnya, pada bagian ketiga dari seri yang disertakan.

Kami adalah programmer yang mengambil pendekatan fungsional. Ini berarti bahwa kami berusaha keras untuk membangun fungsi kompleks dari fungsi lain - kecil dan sederhana. Sejauh ini, dalam mempertimbangkan berbagai opsi untuk menyelesaikan masalah, kami telah mengurangi jumlah variabel perantara. Akibatnya, kode solusi menjadi lebih sederhana dan lebih mudah. Tetapi bagaimana jika ide ini dianggap ekstrem? Bagaimana jika Anda mencoba untuk menyingkirkan semua variabel perantara? Dan bahkan mencoba untuk menjauh dari beberapa parameter?

Anda bisa membuat fungsi untuk menghitung rata-rata menggunakan fungsi compose() saja, tanpa menggunakan variabel. Kami menyebutnya "pemrograman tanpa menggunakan notasi berbutir halus" atau "pemrograman implisit". Untuk menulis program seperti itu, Anda akan memerlukan banyak fungsi tambahan.

Terkadang kode seperti itu mengejutkan orang. Ini disebabkan oleh kenyataan bahwa pendekatan semacam itu sangat berbeda dengan pendekatan yang diterima secara umum. Tetapi saya menemukan bahwa menulis kode dengan gaya pemrograman implisit adalah salah satu cara tercepat untuk memahami esensi pemrograman fungsional. Karena itu, saya dapat menyarankan Anda untuk mencoba teknik ini di beberapa proyek pribadi. Tetapi saya ingin mengatakan bahwa mungkin Anda tidak boleh menulis dengan gaya pemrograman implisit kode yang harus dibaca orang lain.

Jadi, kembali ke tugas kita membangun sistem untuk menghitung rata-rata. Demi menghemat ruang, kami akan pindah ke sini untuk menggunakan fungsi panah. Biasanya, sebagai aturan, lebih baik menggunakan fungsi bernama. Inilah artikel bagus tentang topik ini. Ini memungkinkan Anda untuk mendapatkan hasil penelusuran jejak yang lebih baik jika terjadi kesalahan.

 //   // ---------------------------------------------------------------------------- const filter = p => a => a.filter(p); const map   = f => a => a.map(f); const prop  = k => x => x[k]; const reduce = r => i => a => a.reduce(r, i); const compose = (...fns) => (arg) => fns.reduceRight((arg, fn) => fn(arg), arg); //  -   "blackbird combinator". //     : https://jrsinclair.com/articles/2019/compose-js-functions-multiple-parameters/ const B1 = f => g => h => x => f(g(x))(h(x)); //  // ---------------------------------------------------------------------------- //   sum,    . const sum = reduce((a, i) => a + i)(0); //     . const length = a => a.length; //       . const div = a => b => a / b; //   compose()        . //    compose()     . const calcPopularity = compose(    B1(div)(sum)(length),    map(prop('popularity')),    filter(prop('found')), ); const averagePopularity = calcPopularity(victorianSlang); console.log("Average popularity:", averagePopularity); 

Jika semua kode ini menurut Anda benar-benar omong kosong - jangan khawatir tentang hal itu. Saya memasukkannya di sini sebagai latihan intelektual, dan tidak mengecewakan Anda.

Dalam hal ini, pekerjaan utama adalah fungsi compose() . Jika Anda membaca kontennya dari bawah ke atas, ternyata perhitungan dimulai dengan memfilter array berdasarkan properti elemen yang found . Lalu kami mengambil properti elemen popularity menggunakan map() . Setelah itu kami menggunakan apa yang disebut " blackbird combinator ". Entitas ini direpresentasikan sebagai fungsi B1 , yang digunakan untuk melakukan dua lintasan perhitungan pada satu set data input. Untuk lebih memahami hal ini, lihat contoh berikut:

 //   ,  , : const avg1 = B1(div)(sum)(length); const avg2 = arr => div(sum(arr))(length(arr)); const avg3 = arr => ( sum(arr) / length(arr) ); const avg4 = arr => arr.reduce((a, x) => a + x, 0) / arr.length; 

Sekali lagi, jika Anda tidak mengerti apa-apa lagi - jangan khawatir. Ini hanya demonstrasi bahwa JavaScript dapat ditulis dengan cara yang sangat berbeda. Dari fitur-fitur ini, ini adalah keindahan bahasa ini.

5. Memecahkan masalah dalam satu lulus dengan perhitungan nilai rata-rata kumulatif


Semua konstruksi perangkat lunak di atas melakukan pekerjaan dengan baik untuk menyelesaikan masalah kita (termasuk siklus imperatif). Mereka yang menggunakan metode .reduce() memiliki kesamaan. Mereka didasarkan pada pemecahan masalah menjadi fragmen kecil. Fragmen-fragmen ini kemudian dirakit dengan berbagai cara. Dengan menganalisis solusi ini, Anda mungkin memperhatikan bahwa di dalamnya kita berkeliling array tiga kali. Ada perasaan bahwa itu tidak efektif. Alangkah baiknya jika ada cara untuk memproses array dan mengembalikan hasilnya dalam satu pass. Metode ini ada, tetapi aplikasinya akan membutuhkan menggunakan matematika.

Untuk menghitung nilai rata-rata elemen array dalam satu pass, kita membutuhkan metode baru. Anda perlu menemukan cara untuk menghitung rata-rata menggunakan rata-rata yang dihitung sebelumnya dan nilai baru. Kami mencari metode ini menggunakan aljabar.

Nilai rata-rata dari n angka dapat ditemukan menggunakan rumus ini:


Untuk mengetahui angka rata-rata n + 1 , rumus yang sama akan berlaku, tetapi dalam entri yang berbeda:


Rumus ini sama dengan ini:


Dan hal yang sama seperti ini:


Jika Anda mengonversi ini sedikit, Anda mendapatkan yang berikut:


Jika Anda tidak melihat intinya dalam semua ini, maka tidak apa-apa. Hasil dari semua transformasi ini adalah bahwa dengan bantuan rumus terakhir kita dapat menghitung nilai rata-rata selama satu traversal array. Untuk melakukan ini, Anda perlu mengetahui nilai elemen saat ini, nilai rata-rata yang dihitung pada langkah sebelumnya, dan jumlah elemen. Selain itu, sebagian besar perhitungan dapat dilakukan dalam fungsi peredam:

 //      // ---------------------------------------------------------------------------- function averageScores({avg, n}, slangTermInfo) {    if (!slangTermInfo.found) {        return {avg, n};       return {        avg: (slangTermInfo.popularity + n * avg) / (n + 1),        n:  n + 1,    }; } //  // ---------------------------------------------------------------------------- //       . const initialVals    = {avg: 0, n: 0}; const averagePopularity = victorianSlang.reduce(averageScores, initialVals).avg; console.log("Average popularity:", averagePopularity); 

Berkat pendekatan ini, nilai yang diperlukan dapat ditemukan melewati array hanya sekali. Pendekatan lain menggunakan satu pass untuk memfilter array, yang lain untuk mengekstrak data yang diperlukan dari itu, dan yang lain untuk menemukan jumlah nilai-nilai elemen. Di sini, semuanya cocok menjadi satu pass melalui array.

Harap dicatat bahwa ini tidak selalu membuat perhitungan lebih efisien. Dengan pendekatan ini, lebih banyak perhitungan harus dilakukan. Ketika setiap nilai baru tiba, kami melakukan operasi penggandaan dan pembagian, melakukan ini untuk mempertahankan nilai rata-rata saat ini dalam keadaan saat ini. Dalam solusi lain untuk masalah ini, kami membagi satu nomor menjadi yang lain hanya sekali - di akhir program. Tetapi pendekatan ini jauh lebih efisien dalam hal penggunaan memori. Array perantara tidak digunakan di sini, akibatnya kita harus menyimpan dalam memori hanya objek dengan dua nilai.

. . , . . , , .

?


? , . , - . , , , . , . , . , , .

, - , . , ? . . — .


:

  1. .reduce() .
  2. .filter() .map() , — .reduce() .
  3. , .
  4. .
  5. .

, -, ? — . - — , :

  1. , . — .
  2. , , — .
  3. , , — , .

! JavaScript-?

Source: https://habr.com/ru/post/id458030/


All Articles