Funkcionális programozás JavaScript-ben, magyarázat egyszerű nyelven

Az egyik legnehezebb dolog, amit meg kell tennie a programozásban, a bonyolult vezérlés. Alapos megfontolás nélkül a program mérete és összetettsége odáig nőhet, hogy még a program készítőjét is megzavarja.

Valójában, ahogy az egyik szerző megfogalmazta:

"A programozás művészete a komplexitás ellenőrzésének képessége" - Marijn Haverbeke

Ebben a cikkben lebontunk egy fő programozási koncepciót. Ez a programozási koncepció segíthet az összetettség kordában tartásában és jobb programok megírásában.

A cikk végére megtudhatja, mi is a funkcionális programozás, a létező függvénytípusok, a funkcionális programozás alapelvei, és jobban megismeri a magasabb rendű funkciókat.

Feltételezem, hogy már rendelkezik a funkciók alapjainak előzetes ismereteivel. A funkciók alapvető fogalmaival ez a cikk nem foglalkozik.

Ha gyorsan át szeretné tekinteni a JavaScript funkcióit, akkor itt írtam egy részletes cikket.

Mi a funkcionális programozás?

A funkcionális programozás egy olyan programozási paradigma vagy programozási stílus, amely nagyban függ a tiszta és elszigetelt függvények használatától.

Ahogy a névből is kitalálhatta, a funkciók használata a funkcionális programozás fő alkotóeleme. De pusztán a függvények használata nem jelent funkcionális programozást.

A funkcionális programozás során tiszta funkciókat használunk, amelyek olyan funkciók, amelyeknek nincs mellékhatása. Elmagyarázom, mit jelent mindez.

Mielőtt jobban belemerülne a cikkbe, értsük meg néhány terminológiát és függvénytípust.

A funkciók típusai

A funkcióknak négy fő típusa van.

Első osztályú funkciók

A JavaScript-ben minden funkció első osztályú függvény. Ez azt jelenti, hogy úgy kezelhetők, mint bármely más változót.

Az első osztályú függvények olyan függvények, amelyek értékekként rendelhetők a változókhoz, visszaküldhetők más függvényekből, és argumentumként átadhatók más függvényeknek.

Tekintsük a változónak átadott függvénynek ezt a példáját:

const helloWorld = () => { console.log("Hello, World"); // Hello, World }; helloWorld(); 

Visszahívási funkciók

A visszahívási függvények olyan függvények, amelyeket argumentumként átadnak más függvényeknek, és az a függvény hívja meg őket, amelyben átadják őket.

Egyszerűen a visszahívási függvények olyan függvények, amelyeket más függvényekben argumentumként írunk. Nem hívhatjuk meg a visszahívási funkciókat. Akkor hívják meg őket, amikor meghívják a fő funkciót, amelyben argumentumként adták át őket.

Nézzünk meg egy példát:

const testValue = (value, test) => { if (test(value)) { return `${value} passed the test`; } else return `${value} did not pass the test`; }; const checkString = testValue('Twitter', string => typeof string === 'string'); checkString; // Twitter passed the test 

testValueegy olyan függvény, amely elfogad egy értéket és egy visszahívási függvény, test  amely az "teszten túli értéket" adja vissza, ha az érték igaz lesz, amikor átadják a visszahívási funkciónak.

Ebben az esetben a visszahívási függvény a második argumentum, amelyet átadtunk a testValuefüggvénynek. A testValuefüggvény meghívásakor hívják meg.

Magasabb rendű funkciók

A magasabb rendű függvények olyan függvények, amelyek argumentumként más funkciókat kapnak, vagy egy függvényt adnak vissza.

Ebben a cikkben tovább részletezem a magasabb rendű függvényeket, és azt, hogy miért ilyen erős rendelkezések. Egyelőre csak annyit kell tudnia, hogy az ilyen típusú függvények más funkciókat kapnak argumentumként vagy return függvényként.

Aszinkron funkciók

Az aszinkron függvények olyan funkciók, amelyeknek nincs neve, és nem használhatók fel újra. Ezeket a funkciókat általában akkor írják meg, amikor valamit egyszer és egyetlen helyen kell végrehajtanunk.

Az aszinkron funkció tökéletes példája az, amit korábban a cikkben írtunk.

const checkString = testValue('Twitter', value => typeof value === 'string'); checkString; // Refer to previous code snippet

checkStringolyan változó, amelynek értéke függvény. Két érvet adunk át ebbe a függvénybe.

'Twitter'az első argumentum, a második pedig aszinkron függvény. Ennek a függvénynek nincs egy neve, és csak egy feladata van: annak ellenőrzése, hogy az adott érték karakterlánc-e.

Principles Meme

A funkcionális programozás alapelvei

A cikkben korábban arra utaltam, hogy pusztán a függvények használata nem jelenti a funkcionális programozást.

Van néhány alapelv, amelyet meg kell értenünk, ha programjaink megfelelnek a funkcionális programozási szabvány követelményeinek. Nézzük ezeket.

Kerülje a mutációkat és a mellékhatásokat.

A funkcionális programozás első alapelve a dolgok megváltoztatásának elkerülése. A függvény nem változtathat meg semmit, például egy globális változót.

Ez nagyon fontos, mert a változások gyakran hibákhoz vezetnek. Ha egy függvény például megváltoztat egy globális változót, az váratlan viselkedéshez vezethet minden olyan helyen, ahol a változót használják.

A második elv az, hogy a függvénynek tisztának kell lennie, vagyis nincs mellékhatása. A funkcionális programozásban a végrehajtott változásokat mutációknak, az eredményeket pedig mellékhatásoknak nevezzük.

A pure function does neither of the two. A pure function will always have the same output for the same input.

If a function depends on a global variable, that variable should be passed to the function as an argument. This allows us to obtain the same output for the same input.

Here is an example:

const legalAgeInTheUS = 21; const checkLegalStatus = (age, legalAge) => { return age >= legalAge ? 'Of legal age.' : 'Not of legal age.'; }; const johnStatus = checkLegalStatus(18, legalAgeInTheUS); johnStatus; // Not of legal age legalAgeInTheUS; // 21 

Abstraction

Abstractions hide details and allow us to talk about problems at a higher level without describing all the implementation details of the problem.

We use abstractions in all almost all aspects of our lives, especially in speech.

For example, instead of saying "I'm going to exchange money for a machine that once plugged in displays moving images accompanied with sound", you are most likely to say "I'm going to buy a television".

In this case buy and television are abstractions. These forms of abstractions make speech a lot more easier and reduce the chances of saying the wrong thing.

But you'll agree with me that before using abstract terms like buy you need to first understand the meaning of the term and the problem it abstracts.

Functions allow us to achieve something similar. We can create functions for tasks that we are most likely to repeat again and again. Functions allows us to create our own abstractions.

On top of creating our own abstractions, some functions have already been created for us to abstract tasks that we are most likely to do time and again.

So we are going to look at some of these higher order functions that already exist to abstract repetitive tasks.

Filtering Arrays

When working with data structures like arrays, we are most likely to find ourselves in a situation where we are only interested in certain items in the array.

To obtain these items we can easily create a function to do the task:

function filterArray(array, test) { const filteredArray = []; for (let item of array) { if (test(item)) { filteredArray.push(item); } } return filteredArray; }; const mixedArray = [1, true, null, "Hello", undefined, "World", false]; const onlyStrings = filterArray(mixedArray, item => typeof item === 'string'); onlyStrings; // ['Hello', 'World'] 

filterArray is a function that accepts an array and a callback function. It loops through the array and adds the items that pass the test in the callback function into an array called filteredArray.

Using this function we are able to filter an array and return items that we're interested in, such as in the case of mixedArray.

Imagine if we had 10 different programs and in each program we needed to filter an array. Sooner or later it would become extremely tiresome to rewrite the same function over and over again.

Luckily someone already thought about this. Arrays have a standard filter method. It returns a new array with the items in the array it receives that pass the test that we provide.

const mixedArray = [1, true, null, "Hello", undefined, "World", false]; const stringArray = mixedArray.filter(item => typeof item === 'string') stringArray; // ['Hello', 'World'] 

Using the standard filter method we were able to achieve the same results we did when we defined our own function in the previous example. So, the filter method is an abstraction of the first function we wrote.

Transforming Array Items With Map

Imagine another scenario where we have an array of items but we would like to perform a certain operation on all the items. We can write a function to do this for us:

function transformArray(array, test) { const transformedArray = []; for (let item of array) { transformedArray.push(test(item)); } return transformedArray; }; const ages = [12, 15, 21, 19, 32]; const doubleAges = transformArray(ages, age => age * 2); doubleAges; // [24, 30, 42, 38, 64]; 

Just like that we  have created a function that loops through any given array and transforms all the items in the array based on the callback function the we provide.

But again this would grow tedious if we had to rewrite the function in 20 different programs.

Again, someone thought about this for us, and luckily arrays have a standard method called map which does the same exact thing. It applies the callback function on all the items in the given array and then it returns a new array.

const ages = [12, 15, 21, 19, 32]; const doubleAges = ages.map(age => age * 2); doubleAges; // [24, 30, 42, 38, 64]; 

Reducing Arrays with Reduce

Here's another scenario: You have an array of numbers, but you would like to compute the sum of all these numbers and return it. Of course you can write a function to do this for you.

function reduceArray(array, test, start) { let sum = start; for (let item of array) { sum = test(sum, item) } return sum; } let numbers = [5, 10, 20]; let doubleNumbers = reduceArray(numbers, (a, b) => a + b, 0); doubleNumbers; // 35 

Similar to the previous examples we just looked at, arrays have a standard reduce method that has the same logic as the function we just wrote above.

The reduce method is used to reduce an array to a single value based on the callback function that we provide. It also takes an optional second argument which specifies where we want the operation in the callback to start from.

The callback function we provide in the reduce function has two parameters. The first parameter is the first item in the array by default. Otherwise it is the second argument we provide into the reduce method. The second parameter is the current item in the array.

let numbers = [5, 10, 20]; let doubleNumbers = numbers.reduce((a, b) => a + b, 10); doubleNumbers; // 45 //The above example uses the reduce method to add all the items in the array starting from 10.

Other Useful Array Methods

Array.some()

All arrays have the some method which accepts a callback function. It returns true if any element in the array passes the test given in the callback  function. Otherwise it returns false:

const numbers = [12, 34, 75, 23, 16, 63] console.log(numbers.some(item => item < 100)) // true

Array.every()

The every method is the opposite of the some method. It also accepts a callback function and returns true if all the items in the array pass the test given in the callback  function. Otherwise it returns false:

const numbers = [12, 34, 75, 23, 16, 63] console.log(numbers.every(item => item < 100)) // true

Array.concat()

The concat method, short for concatenate, is a standard array method that concatenates or joins two arrays and returns a new array:

const array1 = ['one', 'two', 'three']; const array2 = ['four', 'five', 'six']; const array3 = array1.concat(array2); array3; // [ 'one', 'two', 'three', 'four', 'five', 'six' ]

Array.slice()

The slice method is an array method which copies the items of an array from a given index and returns a new array with the copied items. The slice method accepts two arguments.

Az első argumentum megkapja az indexet, ahonnan a másolást meg lehet kezdeni. A második argumentum megkapja az indexet, ahonnan le lehet állítani a másolást. Új tömböt ad vissza a másolt elemekkel a kezdő indexből (kizárólagos) a végső indexbe (beleértve).

Ne feledje azonban, hogy a szelet metódus nem használ nulla indexelést. Tehát az első tömbelem indexe 1, nem 0:

const numbers = [1,2,3,4,5,7,8]; console.log(theArray.slice(1, 4)); // [ 2, 3, 4 ] 

Következtetés

Remélem, hogy tetszett olvasni ezt a cikket, és egyszerre tanult valami újat.

Rengeteg tömb- és karakterlánc-módszer létezik, amelyeket nem említettem a cikkben. Ha szeretné, szánjon egy kis időt a módszerek kutatására.

Ha kapcsolatba akar lépni velem, vagy csak köszönni? nyugodtan tegye meg ezt a Twitteren keresztül. Érdekes tippeket és forrásokat is megosztok a fejlesztők számára. ?