Funkcionális programozás JavaScriptben
Folytatom a JavaScript bemutatását, a függvények és az objektumorientált programozás után most a funkcionális programozásra fókuszálva.
A funkcionális programozás egy programozási paradigma – meghatározza milyen elemekből épül fel a programunk. A funkcionális program minden számítást matematikai függvényekkel ír le, alapja a lambda-kalkulus:
A legnépszerűbb funkcionális programozási nyelvek a Lisp, Scheme, Clojure, Erlang, OCaml, Haskell és az F#. A JavaScript ugyan nem tartozik a tisztán funkcionális nyelvek közé, de alkalmas a paradigma használatára.
Koncepciók
Létezik pár koncepció, ami a funkcionális programozáshoz tartozik, ezek a következők:
Tiszta függvények
Azokat a függvényeket nevezzük tisztának, amelyek akárhányszor is hívjuk meg őket, ugyanarra a bemenetre mindig ugyanazt a kimenetet adják vissza, és amelyeknek nincsen mellékhatása, vagyis a hívó fél szemszögéből nem változik meg a függvénynek átadott érték vagy más állapot (memória vagy I/O). Ezek sok hasznos tulajdonsággal rendelkeznek:
- A függvényhívások eredménye gyorsítótárazható.
- Ha nincs adatbeli függőség két függvény között, akkor hívási sorrendjük felcserélhető, illetve párhuzamosan futtathatók.
- Ha az eredményt nem használjuk, akkor a hívás mindenféle következmény nélkül eltávolítható.
- Ha egy nyelv nem enged meg mellékhatásokat, akkor akármilyen kiértékelési stratégia alkalmazható.
Rekurzió
A rekurzív függvények saját magukat hívják, hogy megvalósítsanak egy műveletet, amíg egy elemi esethez nem érnek, ahol már nem szükséges további rekurzió. Funkcionális nyelvekben legtöbbször az iterációt is rekurzióval valósítják meg:
function each(array, callback, context) {
function _each(array, index, callback, context) {
if (index === array.length) {
return; // elemi eset
} else {
if (index in array) {
callback.call(context, array[index], index, array);
}
_each(array, index + 1, callback, context); // rekurzív hívás
}
}
_each(array, 0, callback, context);
}
function map(array, callback, context) {
function _map(array, index, callback, context, result) {
if (index === array.length) {
return result;
} else {
if (index in array) {
result[index] = callback.call(context, array[index], index, array);
}
return _map(array, index + 1, callback, context, result);
}
}
return _map(array, 0, callback, context, []);
}
Elsődleges és magasabb rendű függvények
A függvények a JavaScriptben elsődleges (first-class) típusok, azaz hozzárendelhetjük őket változókhoz és objektum tulajdonságokhoz, átadhatjuk őket paraméterként más függvényeknek, valamint egy függvény illetve metódus is térhet vissza függvénnyel
…
A magasabb rendű függvények (higher order function) olyan függvények, melyek más függvényekkel dolgoznak, azaz vagy paraméterként várják őket, vagy függvényeket adnak vissza.
Az elsődleges és magasabb rendű függvények lehetővé teszik a részleges alkalmazást (partial application). Ez azt jelenti, hogy a függvény egyes paramétereit előre megadjuk, létrehozva egy új függvényt.
Az ES5 óta a nyelv részét képezi a Function#bind()
, ami a kontextus megadása mellett lehetővé teszi a részleges alkalmazást is:
function add(a, b) {
return a + b;
}
var add10 = add.bind(null, 10);
add10(1); // 11
add.bind(null, 'Hello ')('World!'); // 'Hello World!'
A részleges alkalmazáshoz hasonló a currying. Ha létrehozunk egy „curry-vel fűszerezett” változatot a függvényből, akkor annak minden egyes meghívásával részlegesen alkalmazzuk az átadott paramétereket. Ha az átadott paraméterek száma eléri a függvény argumentumainak számát, akkor pedig visszaadja az eredeti függvény visszatérési értékét a korábban megadott paraméterekkel.
function curry(f, arity) {
arity = arity || f.length;
return function _curry() {
var args = Array.prototype.slice.call(arguments);
if args.length >= arity {
return f.apply(null, args);
} else {
return function curried() {
var _args = Array.prototype.slice.call(arguments);
return _curry.apply(null, args.concat(_args));
};
}
};
}
var curriedAdd = curry(add);
curriedAdd(3)(4); // 7
curriedAdd('Hello ', 'World!'); // 'Hello World!'
Mind a részleges alkalmazás, mind a currying megvalósítható jobbról is, azaz az átadott paraméterek a paraméterlista vége helyett az elejére kerülnek.
function partialRight(f) {
var args = Array.prototype.slice.call(arguments, 1);
return function _partialRight() {
var _args = Array.prototype.slice.call(arguments)
return f.apply(null, _args.concat(args));
}
}
function curryRight(f, arity) {
arity = arity || f.length;
return function _curry() {
var args = Array.prototype.slice.call(arguments);
if (args.length >= arity) {
return f.apply(null, args);
} else {
return function curried() {
var _args = Array.prototype.slice.call(arguments)
return _curry.apply(null, _args.concat(args))
};
}
};
}
partialRight(add, 'World!')('Hello '); // 'Hello World!'
curryRight(add)('World!')('Hello '); // 'Hello World!'
Tömbök
A ECMAScript 5-ös változata rengeteg segítséget ad a funkcionális programozáshoz. Feljebb már bemutattam, hogy létezik beépített eszköz a részleges alkalmazásra, de a nyelv tömbök esetén is sok segítséget ad a paradigma gyakorlásához (Array#extras). Ezek végigiterálnak a tömbön, minden egyes elemre meghívva az átadott függvényt.
Array#forEach()
Az Array#forEach()
hívásakor nem igazán tudunk tiszta függvényt átadni, de attól még az azt használó függvényünk maradhat tiszta, ha csak az átadott függvény végez I/O-t. Az átadott függvény három paramétert kap: az aktuális elem, az elem sorszáma és maga a tömb. Például, hogyha ki akarjuk íratni az tömbünk elemeit:
[1, 2, 3, 4].forEach(console.log.bind(console, 'Element:'));
// > Element: 1
// > Element: 2
// > Element: 3
// > Element: 4
Array#map()
Az Array#map()
segítségével egy új tömböt hozhatunk létre a már meglévő elemeit használva. Három paramétert ad át a függvénynek, hasonlóan az Array#forEach()
-hez, és a visszaadott érték lesz az új tömb vonatkozó eleme.
[1, 2, 3, 4].map(function elementTimesIndex(element, index) {
return element * index;
}); // [0, 2, 6, 12]
[1, 2, 3, 4].map(add); // [1, 3, 5, 7]
Array#filter()
Az Array#filter()
segítségével egy olyan tömböt hozhatunk létre, melyben csak azokat az elemeket tartjuk meg, amelyek eleget tesznek a feltételnek. Három paramétert ad át a függvénynek, hasonlóan az Array#forEach()
-hez. Például, ha csak a páratlan elemeket szeretnénk megtartani:
function isOdd(n) {
return n % 2 === 1;
}
[1, 2, 3, 4].filter(isOdd); // [1, 3]
Hogyan tudnánk erre építve létrehozni egy olyan függvényt, ami a párosakat adja vissza?
function negate(predicate) {
return function() {
!predicate.apply(this, arguments);
};
}
var isEven = negate(isOdd);
[1, 2, 3, 4].filter(isEven); // [2, 4]
Array#reduce() és Array#reduceRight()
Az Array#reduce()
segítségével a tömbünket egyetlen értékre redukálhatjuk. Paraméterként egy függvényt és opcionális kezdőértéket vár. Négy paramétert ad át a függvénynek: az előző érték, az aktuális elem, az elem sorszáma és a tömb. A függvénynek az új értéket kell visszaadnia. Ha nem adunk meg kezdőértéket, akkor a kezdőérték az első elem lesz, és az iteráció a második elemmel indul. Segítségével könnyen összeadhatjuk a tömbünk elemeit:
[1, 2, 3, 4].reduce(add); // 10
[' ', 'World', '!'].reduce(add, 'Hello'); // 'Hello World!'
Az Array#reduceRight()
megegyezik a Array#reduce()
-szal, csak a tömb elemeit jobbról balra dolgozza fel. Ha nem adunk meg kezdőértéket, akkor a kezdőérték az utolsó elem lesz, és az iteráció a utolsó előtti elemmel indul.
['!', 'World', ' ', 'Hello'].reduceRight(add); // 'Hello World!'
Array#some() és Array#every()
Az Array#some()
és az Array#every()
az Array#reduce()
egy speciális formája. Az Array#some()
true
értéket ad vissza, ha a tömb legalább egy eleme teljesíti a feltételt. Az Array#every()
pedig akkor ad vissza true
értéket, ha a tömb minden eleme teljesíti a feltételt. Az Array#some()
nem iterál tovább, amennyiben talált egy elemet, ami megfelel a feltételnek. Az Array#every()
pedig akkor, amikor egy elem nem felel meg a feltételnek.
[1, 2, 3, 4].some(isOdd); // true
[1, 3].some(isEven); // false
[1, 2, 3, 4].every(isOdd); // false
[1, 3].every(isOdd); // true
Array#reduce()
segítségével a következőképp írhatjuk fel őket:
function some(array, predicate, context) {
return array.reduce(function (previous) {
var args = Array.prototype.slice.call(arguments, 1);
if (previous === true) {
return true;
} else {
return !!predicate.apply(context, args);
}
}, false);
}
some([1, 2, 3, 4], isOdd); // true
some([1, 3], isEven); // false
function every(array, predicate, context) {
return array.reduce(function (previous) {
var args = Array.prototype.slice.call(arguments, 1);
if (previous === false) {
return false;
} else {
return !!predicate.apply(context, args);
}
}, true);
}
every([1, 2, 3, 4], isOdd); // false
every([1, 3], isOdd); // true
Felhasználás
Funkcionális programozáshoz alapvető, hogy rendelkezzünk egy általánosan használható függvénykönyvtárral, amikből újabb függvényeket készíthetünk. Bemutatok néhány hasznos ilyen függvényt, ezzel bővítve a rendelkezésre álló eszközöket.
Azonosság
Kezdjük pár hasznos, de egyszerű függvénnyel. A legalapabb ezek közül az identity()
, amely visszaadja az első kapott paramétert:
function identity(value) {
return value;
}
[0, 1, 2, null, undefined].filter(identity); // [1, 2]
Konstans
Másik hasznos függvény lehet a constant()
, amely mindig az előre megadott értéket adja vissza:
function constant(value) {
return function () {
return value;
};
}
[1, 2, 3, 4].map(constant(42)); // [42, 42, 42, 42]
Tulajdonság
Hozzunk létre egy függvényt, ami a megadott objektumnak visszaadja egy, tömbbel megadott elérési úton található elemét:
function property(object, path) {
return path.reduce(function (previous, current) {
if (previous !== undefined && previous !== null) {
return previous[current];
} else {
return undefined;
}
}, object);
}
property([1, 2, 3, 4], [1]); // 2
var object = {a: {b: {c: 'd'}}};
property(object, ['a', 'b', 'c']); // "d"
Ezt később felhasználhatjuk arra, hogy generáljunk egy függvényt, ami mindig az adott elérési utat keresi.
function propertyAt() {
var path = Array.prototype.slice.call(arguments);
return function (object) {
return property(object, path);
};
}
var head = propertyAt(0);
head([1, 2, 3, 4]); // 1
var length = propertyAt('length');
length([1, 2, 3, 4]); // 4
Ezután, ha meg akarjuk kapni a kapott tömbök első elemét, vagy elemeinek hosszát, könnyű a dolgunk:
[[1, 2, 3, 4], [5, 6, 7], [8, 9]].map(head); // [1, 5, 8]
['foo', 'bar', 'foobar', 'baz'].map(length); // [3, 3, 6, 3]
['foo', 'bar', 'foobar', 'baz'].map(length).reduce(add); // 15
Ha adott objektum különböző tulajdonságait akarjuk kikeresni, generálhatunk egy másik függvényt:
function propertyOf(object) {
return function () {
var args = Array.prototype.slice.call(arguments);
return property(object, args);
};
}
var object = {a: 1, b: 2, c: 3, d: {e : 4}}
var getter = propertyOf(object);
getter('a') // 1
getter('d', 'e') // 4
Ezek után már csak egy lépés egy általános tulajdonságkiemelő pluck()
függvény felírása:
function pluck(array) {
var path = Array.prototype.slice.call(arguments, 1);
return array.map(function (object) {
return property(object, path);
});
}
var objects = [{name: 'Foo'}, {name: 'Bar'}, {name: 'Bazbar'}]
pluck(objects, 'name'); // ['Foo', 'Bar', 'Bazbar']
Ha meg akarjuk tudni, hogy mindegyik név hosszabb-e, mint két karakter, vagy van-e olyan, ami három karakter hosszú, egyszerű a dolgunk:
function greaterThan(a) {
return function(b) {
return a < b;
}
}
function equals(a) {
return function(b) {
return a === b;
}
}
var nameLengths = pluck(objects, 'name').map(length); // [3, 3, 6]
var equals3 = equals(3);
nameLengths.every(greaterThan(2)); // true
nameLengths.every(equals3); // false
nameLengths.some(equals3); // true
Kompozíció
Előfordulhat, hogy egy értékkel egymás után több műveletet szeretnénk végezni, mindig felhasználva az előző művelet eredményét. Erre való a flow()
:
function flow() {
var functions = Array.prototype.slice.call(arguments);
return function (target) {
return functions.reduce(function(previous, current) {
return current(previous);
}, target);
};
}
function multiply(a, b) {
return a * b;
}
// f(x) = (x + 3) * 2
[0, 1, 2, 3].map(
flow(add.bind(null, 3), multiply.bind(null, 2))
); // [6, 8, 10, 12]
Ha fordított sorrendben akarjuk megadni a függvényeket, akkor szokás a compose()
elnevezéssel élni. Ez megfelel a függvények felírási sorrendjének egy egyenletben.
function compose() {
var args = Array.prototype.slice.call(arguments);
return flow.apply(null, args.reverse());
}
function divide(b, a) {
return a / b;
}
function round(precision) {
if (precision) {
var multiplier = Math.pow(10, precision);
return compose(
divide.bind(null, multiplier),
Math.round,
multiply.bind(null, multiplier)
);
} else {
return Math.round;
}
}
var roundToOneDecimal = round(1);
[.12, .345, 67.89].map(roundToOneDecimal); // [0.1, 0.3, 67.9]
// f(x) = roundToOneDecimal(|cos(pi / 3 * x)|)
[0, 1, 2, 3].map(
compose(
roundToOneDecimal,
Math.abs,
Math.cos,
multiply.bind(null, Math.PI / 3)
)
); // [1, 0.5, 0.5, 1]
Letölthető függvénytárak
A két legnépszerűbb funkcionális függvénytár az Underscore.js és a lodash – utóbbi eredetileg az előbbi újraírt változata volt. Mindkettő rengeteg hasznos eszközt nyújt a funkcionális programozáshoz, utóbbi legtöbb esetben gyorsabb, és több lehetőséggel rendelkezik. Jellemzőjük, hogy általánosságban első paraméterként várják az objektumot, amin a műveletet végzik, hasonlóan a fent definiált each()
és map()
függvényekhez.
Jelentősen különbözik tőlük a Ramda, melynek alapvető tulajdonsága, hogy mindent a currying-re épít. Így például a map()
nem első, hanem utolsó paramétere a tömb. Ezzel lehetővé válik, hogy előre deklaráljuk az átalakítás menetét, majd többször alkalmazzuk különböző tömbökre.
Az Underscore.js-hez hasonló a Highland.js, azzal a hatalmas különbséggel, hogy a műveleteket egy adatfolyamon végzi. Azaz ahogy érkezik be folyamatosan az adat, úgy születik az adatfolyam másik végén az átalakított eredmény.
A Reactive Extensions for JavaScript egy Microsoft által indított projekt, mely sok tekintetben hasonlít a Highland.js-hez, ugyanakkor sokkal több nála. Adatfolyam helyett események folyamára alapoz, és az API az Array#extras-ra épít paraméterezésében is.
Összefoglalás
Mint látható, igen minimális függvényekből komoly rendszert hozhatunk össze. Segítségükkel megkönnyíthetjük az adatfeldolgozást, validálást. A függvényeink egyszerűen tesztelhetők, működésük könnyen ellenőrizhető. Kombinálásukkal igen hasznos újabb függvényeket hozhatunk létre tovább egyszerűsítve az elvégzendő feladaton.
■
Kérdések
Tehát szerintem célszerű lenne pár életből vett példával szemléltetni, összehasonlítva mondjuk a mindenki által ismert imperatív vagy procedurális programozással, hogy hagyományosan ezt így oldanánk meg, de ekkor és ekkor érdemes funkcionálisan programozni, mert azt nyerem vele, hogy ..., ..., ...
Mik a hátrányai? Erről is ritkán esik szó, márpedig enélkül nehéz objektív döntést hozni. Egy array.forEach és egy for ciklus között akár ötven-százszoros különbség is lehet, amire nem nehéz magyarázatot találni: minden függvényhívás memóriafoglalással jár, amit utána fel is kell szabadítania a szemétgyűjtőnek, ami nem egy gyors folyamat. Megéri-e ezt az árat a funkcionális programozás egy olyan nyelvben, ahol nincs natívan támogatva?
hol és miért célszerű
Igen, a kod lassabb lesz, mondjuk nem annyival lassabb, mivel a magában a ciklusban valószínűleg komolyabb számítást végzel mint egy szám növelése, amivel általában tesztelni szokták a sebességkülönbséget. A korszerű JavaScript motorok pedig képesek a tiszta függvényeket inline változatra cserélni, azaz az egész
forEach
ciklust képesek lecserélni egy megfelelőfor
ciklusra.A funkcionális programozás natívan támogatva van JavaScriptben, egyedül a más funkcionális nyelvekben gyakran használt tail call optimalizáció nincs jelenleg, de az ES2015-ben már vannak lépések annak támogatására is.
Mindenkinél mások a példák, mert más feladatokat kell elvégeznie. Ha írnék pár példát amiket használok, valószínűleg nem sok hasznát tudnád venni, mert más alkalmazásokat fejlesztesz, mások a követelmények.
Vegyük a következő feladatot, amiben egy objektumokat tartalmazó tömböt akarunk átalakítani. Ezek mondjuk IoT eszközök adatai, de nem minden percre van minden műszertől adat. Ki akarjuk rajzolni az értékeket egy vonaldiagrammon. Ha az érték nem létezik a bemenetben, akkor oda
null
-t írunk.convert
felírásához lodash-t használok:Mik a hátrányai? 1)
1) Értelmezhetőség: a kód összetettsége miatt nehezen értelmezhető.
2) Karbantarthatóság: egy bizonyos méret felett rémálom karbantartani
3) Moduláris kialakítás: minimális lehetőségek
4) Használhatóság
a) böngészők: egy-két kivételtől eltekintve (játékok, grafika, ..), minimális szükség van funkcionális megoldásokra, eltekintve persze a DOM-al is foglalkozó ilyen-olyan többé-kevésbé funkcionális szerszámokra (jQuery és társai)
b) szerver oldal: ez a terület, ahol már van a létjogosultsága egy bizonyos szintig
Szerintem..
Értelmezhetőség, karbantartás
bind
,apply
éscall
hívásokkal (én ezeket nyelvi bűznek tartom), amik miatt sokat kell agyalni, hogy pontosan mi is történik, indirekt az egész, össze-vissza kell ugrálni, ha meg szeretném érteni.Úgyhogy ez alapján bennem tegnap először az fogalmazódott meg, hogy nem intuitív. A végeredmény lehet, hogy egyszerű, de a hozzá szükséges kód biztosan nem, ami megnehezíti a karbantartást
Ha használsz babel-t, akkor
Rest & Spread operator
Válaszok
Nagy divat lett mostanság a
Véleményem, amit írtam most is tartom..
1) Sokkal bonyolultabb értelmezni egy funkcionális kódot, annál nehezebb minél több egymásba ágyazás van.. meghívsz egy funkciót, ami "belső" függvényt használ, amiben szintén van egy függvény, ami meghív egy sokadikat, ami szintén.. és már csak az ember a fejét fogja, hogy most mit fog visszaadni..
Elhiszem, hogy tömbök, objektumok esetén egyszer kell megírni, csakhogy az életben nem csak tömbök és objektumok vannak.
2) Légy szíves írj már pár "komoly" rendszert ami legnagyobb része bizonyíthatóan funkcionális nyelven van megírva, mert én csak OOP-re kis milliót tudok írni a többiről nem beszélve.
3) Ebben lehet igazad van.. csak akkor nem igazán értem, hogy miként lehet, hogy pl.: a JQuery mind a mai napig nem moduláris, miért van tele olyan "szeméttel", ami más nyelveken modulba illik rakni és ez sajnos igaz, nagyon sok ismert library-ra.
4.a) Rossz példa az SPA, mert nem "gondolok" rá, hanem csinálom, a köze a funkcionális programozáshoz szinte nulla. Sokat elmond, hogy ahhoz képest hogy több száz és ezer elem feldolgozása kell egy online táblázatkezelőben, amit én eddig láttam mind OOP-s volt és nem funkcionális.
Persze ez csak az én véleményem, mindenkinek van joga véleményt alkotni.
Komoly rendszer
Szóval lehetséges, de szerintem ez csak úgy lehetséges, ha az ember eleve funkcionálisan kezd programozni.
Azt mondják, hogy
szerk: Annyira azért mégsem hiányzik már egy jó ideje, nem értem miért írtam ezt a marhaságot.
A funkcionális programozás
flow
és acompose
része a Javascriptnek? Utóbbi nélkül várható, hogy funkcionálisan lassabb lesz, mint procedurálisan, hisz több függvényhívásra van szükség.Méréseim szerint imperatív módon tízszer gyorsabb:
támogatva van ~ a függvények
JS-ben a funkcionális megközelítés nyilván lassabb, szerintem senki sem állította az ellenkezőjét. Kliens oldalon amúgy sem szokás erőforrásigényes programokat íri (olyan programokat, aminél észrevehető lesz ez a sebességkülönbség), a hálózati kérések, a DOM manipulálás több nagyságrenddel lassabb, így gyakorlati szempontból teljesen mindegy, hogy imperatív vagy deklaratív megközelítést használsz...
Ennyi erővel a C-ben is
Amennyit eddig olvastam a témában, a natív funkcionális nyelvek egyik nagy erőssége, hogy egy programon belül minimális számú változóval dolgoznak (tipikusan csak olyanokkal, amelyek I/O műveletek eredményei), a legtöbb érték, amit mi változónak nevezünk, ott megváltoztathatlan (immutable). Ezzel garantálják, hogy egy program a lehető legkevesebb állapotot vehessen fel, amivel jelentősen lecsökkentik a hibázás lehetőségét. Ez szükséges a párhuzamos programfuttatáshoz is.
A változókezelés miatt a funkcionális programozás memóriaigénye is jóval nagyobb a hagyományosnál. Úgy gondolom, hogy a JS-ben használt szemétgyűjtős módszer ennek a kezelésére nagyon nem hatékony.
Ennyi erővel a C-ben is
A mellékhatások elkerülése (ide tartozik az immutability is) azért fontos, mert így egy csomó optimalizációt meg tud tenni a futtatókörnyezet.
fib 30
kiértékelésére, így a fordító ezt megspórolta.[f 1, f 1]
kifejezésben minden további nélkül kicserélhetjük azf 1
-et a kifejezés eredményére (amit csak egyszer kell kiértékelni, hiszen ugyanolyan argumentumokra ugyanazt fogja adni a kifejezés).Az immutability azt az érzést adja, hogy egy objektum módosítása költséges, hiszen magát az objektumot nem tudjuk módosítani, csak újat tudunk készíteni, amihez le kell másolnunk az eredetit. Ám mivel az eredeti nem fog változni, az új objektum nyugodtan hivatkozhat az eredeti objektumra, nem kell fizikailag minden adatot átmásolni.
Gyakorlatilag minden magas szintű nyelv GC-vel operál manuális memóriakezelés helyett (C, C++, Rust jut hirtelen eszembe, amik nem ilyenek), és az ipar tapasztalatai alapján teljesen működőképes megoldás (nyilván vannak olyan területek, ahol szükség van a valósidejű működésre, ott egy GC pause egyszerűen nem fér bele az időbe).
Funkcionálisan programozni ott érdemes, ahol valamilyen adatot szeretnél transzformálni, hiszen a deklaratív megközelítés pont az ilyen dolgokat írja le jól, pl a kedvenc példám:
In functional code, the
https://en.wikipedia.org/wiki/Functional_programming
Nagyjából az az előnye a funkcionális megközelítésnek, hogy stateless. Minden függvényhívás csak a paraméterekkel dolgozik, nem ránt be semmit scope-ból, illetve nem mutálja a paramétereket, hanem újonnan előállított adatot szór visszatérő értékbe. Ez azért hasznos, mert így különösebb erőfeszítés nélkül párhuzamosítható, mondjuk kitehető egy függvény vagy függvény sorozat worker thread-ekbe vagy child process-ekbe. Js-ben erre már van lehetőség egyikre-másikra egy ideje böngészőben és szerver oldalon is. A másik előnye, hogy könnyebb karbantartani, mert nem a scope-ból ránt be dolgokat, mint a procedurális. Az oo-nak többek között ugyanez az előnye megvan, csak nála a context-be injektálunk dolgokat, ahelyett, hogy a scope-ból rántanánk be őket.
Csak úgy btw. a REST is pont emiatt a stateless megközelítés miatt skálázható jobban horizontálisan, mint a SOAP. Minden a request-el jön, ami ahhoz kell, hogy teljesítsük a kérést, nem kell session-ben turkálni szerver oldalon, hogy előrántsuk a client state azon darabkáját, ami kell a kérés teljesítéséhez. Így a végeredmény csak a kéréstől és a resource state-től függ, amit az adatbázis tárol. It is lehet valami ilyesmi analógiát vonni: request = paraméterek, response = visszatérő érték, resource state = context, client state (session) = scope.
Minden függvényhívás csak a
Egy félreértés: a funkcionális függvények is használják a hatókörben látható változókat, az ún. szabad változóik (azok, amelyek értéke nem argumentumokként kerül átadásra) kötésére. Lévén a változók csak egyszer kapnak értéket, ez nem ássa alá a transzparenciát.
És mert transzparens, ezért az értékével behelyettesíthető.
Egy másik (közkeletű) félreértés: a REST nem attól stateless, hogy nem használsz sessiont az alkalmazás szintjén. Egy PHP session, amit a lemezen tárolsz, semmiben sem különbözik a MySQL-ben tárolt adataidtól. A session cookie ugyanolyan paramétere a kérésnek mint a query string. A különbség az, hogy kérés és kérés a protokoll szemszögéből független a HTTP esetén, szemben például az FTP-vel.
Egy félreértés: a
Köszi, ezt nem tudtam. Ezek szerint a skálázhatóságnál az immutability számít igazán.
De a REST attól stateless.
De különbözik, a session részben cache funkciókat lát el, hogy ne kelljen mondjuk a jogosultságokat újra és újra elkérni az adatbázistól ez gyakorlatilag query cache a resource state-hez. A resource state, amit az adatbázisban tárolsz, mondjuk termék lista, felhasználó lista, stb. Ami problémásabb a session-el az a másik fele, ami a client state egy darabkája. Pl hogy logged in a user, vagy hogy éppen a mobil nézetet nézi narancssárga design-al, és nem a desktopot kék design-al, és így tovább. Gyakorlatilag amit egy böngészős REST kliens-nél a monitoron látsz az mind a client state, és mind bekerülhet egy hagyományos webalkalmazásnál a session-be, ami a szerver oldalon tárolódik. A REST = representational state transfer, mint a neve is mutatja arról szól, hogy a client state és a resource state darabkáit reprezentáció formájában küldözgeti egymásnak a kliens és a szerver, és így változtatják meg egymás állapotát. A session használatával a client state egy darabkája átkerül a szerverre. Mivel a client state sokkal gyakrabban változik, mint a resource state, ezért ez egy méret (bizonyos felhasználószám) felett túl nagy terhet ró a szerverre, ami alatt az összeroppan. A public SOAP API-k legnagyobb részt azért buktak el, mert nem tudták megoldani ezt a problémát, illetve valszeg fel sem ismerték a problémát a megalkotóik.
Nem. A query string önmagában értelmes a szerver számára, a session cookie értelmezéséhez szükség van a session storage-ben tárolt, hozzá kapcsolódó adatokra. Ezért ha session cookie-t használsz, akkor nem tudod normálisan használni a HTTP cache-et, mert a kérés nem fog tartalmazni minden információt ahhoz, hogy megfelelően összepárosítható legyen egy adott válasszal. A client state egy töredéke hiányozni fog, mert a cache-nél nem került mentésre, a session storage-ben meg azóta már megváltozott az értéke. Ha csak simán cookie-ba teszel információt, akkor az használható lehet, feltéve, hogy a HTTP cache menti a cookie-t is, és felhasználja egy-egy kérés-válasz összepárosításánál. Ebben nem vagyok biztos, nem szoktam cookie-t használni REST-el. Úgy rémlik, hogy van egy szűk lista, amit felhasznál, és ami nagyjából method + uri + auth header + accept header, vagy valami ilyesmi. Nem rémlik, hogy a cookie benne lenne, de utána fogok nzéni.
Igen, ez a különbség a HTTP és az FTP protokollok között. A REST és egy hagyományos webalkalmazás között meg többek között az, hogy a REST nem tárol a szerveren client state-hez tartozó információkat, a hagyományos webapp meg igen. Így a REST stateless, a hagyományos webapp meg stateful kéréseket küld. A protokoll itt csak annyit számít, hogy a HTTP lehetőséget ad stateless kérések küldésére.
szerk:
ugyanez rövidebben:
http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_3
We next add a constraint to the client-server interaction: communication must be stateless in nature, as in the client-stateless-server (CSS) style of Section 3.4.3 (Figure 5-3), such that each request from client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. Session state is therefore kept entirely on the client.
Figure 5-3: The client-stateless-server style
This constraint induces the properties of visibility, reliability, and scalability. Visibility is improved because a monitoring system does not have to look beyond a single request datum in order to determine the full nature of the request. Reliability is improved because it eases the task of recovering from partial failures [133]. Scalability is improved because not having to store state between requests allows the server component to quickly free resources, and further simplifies implementation because the server doesn't have to manage resource usage across requests.
Like most architectural choices, the stateless constraint reflects a design trade-off. The disadvantage is that it may decrease network performance by increasing the repetitive data (per-interaction overhead) sent in a series of requests, since that data cannot be left on the server in a shared context. In addition, placing the application state on the client-side reduces the server's control over consistent application behavior, since the application becomes dependent on the correct implementation of semantics across multiple client versions.
szerk:
Nem derült ki, hogy alapból nézi e a cookie tartalmát a cache. Valszeg nem, mert láttam SO-n kérdést ezzel kapcsolatban, de ki kellene próbálni. Ami kiderült, hogy a "vary: cookie" header-el biztosan beállítható a dolog. Néhány böngésző régen (5+ év) még bugos volt ezen a téren, úgy néztem, de azóta ezeket már biztosan javították.
https://tools.ietf.org/html/rfc7234#page-8
When presented with a request, a cache MUST NOT reuse a stored
response, unless:
o The presented effective request URI (Section 5.5 of [RFC7230]) and
that of the stored response match, and
o the request method associated with the stored response allows it
to be used for the presented request, and
o selecting header fields nominated by the stored response (if any)
match those presented (see Section 4.1), and
https://tools.ietf.org/html/rfc7234#section-4.1
When a cache receives a request that can be satisfied by a stored
response that has a Vary header field (Section 7.1.4 of [RFC7231]),
it MUST NOT use that response unless all of the selecting header
fields nominated by the Vary header field match in both the original
request (i.e., that associated with the stored response), and the
presented request.
https://tools.ietf.org/html/rfc7231#section-7.1.4
request message, aside from the method, Host header field, and
request target, might influence the origin server's process for
selecting and representing this response. The value consists of
either a single asterisk ("*") or a list of header field names
(case-insensitive).
a session részben cache
Ez egy implementációs részlet, milyen különbséget jelent ez a kliens szempontjából?
Ez mind ugyanúgy egy resource (az alkalmazás session) állapota, mint egy terméklista elemei. Az, hogy a listának mely elemeit látod, egy kérés paraméter függvénye, amit valószínűleg a query stringben küldesz; az, hogy a narancssárga mobilnézetet látod-e, ugyanúgy egy kérés paraméter függvénye, amit valószínűleg egy cookie-ban küldesz.
Talán még emlékszel, hogy egy időben dívott a session id-t query paraméterben küldeni… Hogy is van ez? :)
Erre való a
Vary
header :)Ez így még megáll a lábán, habár ezzel csak annyit mondtál, hogy a felhasználói adatokat kezelő alkalmazásoknak nagyobb a terhelése, mert több adatot kezelnek, mint a felhasználó nélküliek. Ezzel sokra nem megyünk. Azonban átvezet a problémához:
A REST egy hálózati architektúra stílus, amit Roy Fieldingék a HTTP/1.1 tervezésekor fogalmaztak meg, és a HTTP/1.1-gyel megvalósítottak. Ebben az értelemben lehetetlen HTTP-t használva nem REST-et használni. A zavar abból adódik, hogy össze van mosva a sztenderd, stateless kommunikációs protokoll (a HTTP) és a felette megfogalmazott, egyedi, stateful protokoll (a webalkalmazás). A REST elvek mentén működő web szempontjából a kommunikáció stateless, mert a webszervernek nem kell állapotot tárolnia két kérés között. Az általad, az alkalmazáslogikád képében megfogalmazott protokoll azonban természetesen stateful, mert máshogy nem volna sok haszna.
A perspektíva kedvéért: a stateless HTTP alatt egyébként a TCP stateful, ami alatt az IP stateless.
Ez egy implementációs
Semmilyen, éppen ezért ez a része maradhat a szerveren REST esetében, a másik, ami a klienshez tartozik, meg muszáj, hogy átkerüljön oda.
Hát ez bekerülhetne az aranyköpések közé. :D
Csak hogy tisztázzuk az elnevezéseket: client state = session state = application state, amik szinonímák. Mindhárom a kliens állapotát jelenti, amit mindig az aktuális kliens tart karban REST esetében. Hagyományos webappok esetében vegyesen a kliens és a szerver tartja karban, az utóbbi egy session storage-ben. A resource state, amit a szerver tart karban legtöbbször adatbázisban.
Úgy, hogyha kiragadod kontextusból, amit írok, akkor jelenthet egész mást. Nincs kedvem ismételni magam egyébként sem annak van jelentősége, hogy a request melyik részében küldöd el a session id-t, hanem hogy áthelyezi a client state és a kérés egy darabját a szerverre, amire pedig szükség volna a kesseléshez.
Nem, nem erre való a vary header.
Pedig pont ez a lényege a stateless constraint-nek, de láthatóan ez nem jut el hozzád. Ha egy public API a világ minden pontjáról fogad kéréseket, és egyszerre több millióan használják, akkor ha a szervernek kell karban tartani az összes kliens állapotát, akkor összeomlik a terhelés alatt, és ez a probléma skálázással nem megoldható a jelenlegi technológiákkal. Ennyi a történet.
Nem értem ezeket a kijelentéseidet mire alapozod? Hadd idézzek neked Roy Fielding-től:
- http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
A resource state, amit a
Ugye érzed, hogy ez így definíciónak nem lesz jó?
Mellesleg a client state-et sem definiáltad még. A cache-elést mi akadályozza?
Valóban? Akkor mire? :)
Valóban, ha felhasználókhoz köthető adatokat tárolsz, és sok felhasználód van, akkor sok adatod lesz. Tényleg nem értem, a javasolt megoldás szerinted az, hogy akkor ne tároljunk adatokat? Miért csak a felhasználókhoz köthető adatoktól akarunk megszabadulni?
Fielding doktori disszertációjára.
Ja, egy ponton túl szerintem eléggé túlpörgi ő maga is. De persze lehet, hogy csak nem értem, lásd a fenti kérdéseim.
Valóban, ha felhasználókhoz
Ezek nem felhasználóhoz köthető adatok, hanem munkamenethez köthető adatok. A kettő nagyon más. A felhasználóhoz köthető adatokat tárolhatod szerveren REST-nél, a munkamenethez köthetőeket viszont a kliens kell, hogy karbantartsa.
Fielding sosem írt ilyet a disszertációjában, hogy a HTTP 1.1 megvalósítja a REST-et. Te értetted félre. Abban igazad van, hogy a HTTP 1.1-et úgy tervezték, hogy megfeleljen a REST-nek. Ez nem véletlen. A disszertáció azt írja le, hogy hogyan kellene használni a HTTP 1.1-et, mi volt a cél, amikor megtervezték a szabványt. A REST ezen kívül használhat más stateless protocol-t is, de kevés olyan jó van hozzá, mint a HTTP, ezért nem szokás ilyesmit meglépni.
A REST uniform interface constraint-jének a lényege, hogy meglévő standard-ekből építesz fel egy interface-t a webalkalmazásodhoz, pl. HTTP + URI + RDF + RDF vocabs, de használhatsz más standard-eket is ha az jobban esik. Ilyen módon lazán csatolt lesz a kliens a szerver implementációjához, mert csak az interface-hez kell szorosan csatoltnak lennie. Nem sokban különbözik ez a polimorfizmustól, ha belegondolsz. A különbség csak a méretekben van: a REST-nél webalkalmazásokról meg szabványokból összerakott uniform interface-ről van szó, amit használnak, a polimorfizmusnál meg osztályokról és az interface-ről, amit megvalósítanak.
Írtam még régebben 2 blog bejegyzést a témában, ha gondolod olvasd át őket: 1 2
Ezek nem felhasználóhoz
Milyen kritérium alapján döntesz, amikor tárolni kell valamit, hogy a kliensen vagy a szerveren helyezed el? Hol a határ a munkamenet és a profil között?
A munkamenetes adatok
Egy félreértés: a
Ha itt a closure változókról van szó, akkor azt semmi nem garantálja, hogy csak egyszer kapnak értéket. Azok state-ek. Legalábbis JS-ben. Vannak szigorúbb funkcionális nyelvek, ahol nem változhatnak, de az nem JS.
Ezért egyébként a JS closure egy picit ellentmondásos jelenség, mivel olyan szempontból a funkcionális paradigba reprezentánsa, hogy a functions-are-first-class-citizen elvre épül, olyan szempontból viszont nem, hogy a lexical scope változói kőkemény state-ek.
Ha itt a closure változókról
Mi az állításod? :)
Hogy garantálható
Ezek a closure-től teljesen
Ha jól értem, ez egy ab ovo kijelentés, miszerint a closure biztosítja a scope változók immutabilitását - na ez nyelvfüggő. Az nem volt odaírva zárójelbe, hogy "mert amúgy a user saját magának garantálta" :)
Ha itt a closure változókról
Az eredeti állítás általában a funkcionális programozás, nem pedig a JavaScriptben történő funkcionális programozás kontextusában lett megfogalmazva. JavaScriptben az argumentumok változatlanságát sem garantálja semmi.
A funkcionális programozás mint paradigma maga hordozza ezt a ketősséget: eredetileg a függvényekkel mint elsődleges adattípusokkal történő programozásról volt szó (lásd Lispek), később kapcsolódott hozzá az egyszeri értékadás filozófiája (Hope és követői).
Mindennel egyetértek, egy
Ennek nem sok jelentősége van, mert az életciklusuk így is úgy is a függvényen belül marad, tehát nem state-ek.
Itt a kérdés az, hogy a scope változók state-ek vagy sem. JS-ben state-ek (PHP-ban nem).
Abból a szempontból igazad
az objektumok azonban
Ez nem így van, illetve a megfogalmazásod félrevezető. Objektumok esetében nincs másik argumentum pass policy. Ha referenciaként kerülnének átadásra, akkor reassignolni lehetne őket belülről úgy, hogy annak hatása kívül megmaradna. Ahogyan ez PHP-ban is történik a referenciaargumentumokkal, függetlenül attól, hogy primitívek, vagy sem.
Az már igaz, hogy a rajtuk "keresztül" végzett értékadás meg tud maradni, ha ez alatt azt értjük, hogy az objektum egy alkulcsára assignolunk, de ennek oka nem az, hogy az objektum referenciaként került volna átadásra, hanem az a triviális dolog, hogy az objektum nem más, mint egy referencia.
A referencia viszont, mint olyan, általában véve is egy olyan joker ebben a témában, amivel mindenféle immutability paradigmába state csempészhető, JavaScripttől teljesen függetlenül. Bár mondjuk a JavaScriptes Object.freeze-re is rámondható, hogy az alkulcsok mutábilisek maradnak, de ott van a szigorúbb closure policyt megvalósító PHP reference arg rendszere is, ami a closure változókra is él, illetve ahogy nézem, még a Haskellben is lehet szórakozni: http://stackoverflow.com/a/9422659
Itt a nagy különbség az, hogy a closure változók érték vagy referencia szerint vannak bindolva, illetve hogy mennyire könnyű ezen a skálán mozogni. Az egyik véglet a Haskell: érték szerint, de workaroundolható hozzá referencia. Középen áll a PHP: a szokásos opt-in rendszerével (& prefix), a túlszélen pedig ott a JS, ahol referenciaként vannak bindolva a closure változók, emiatt kifejezetten nehézkes garantálni a stateless jelleget.
Annyira próbáltam
A JavaScript (ahogy a Java) változói objektumra nem, csak mutatókra tudnak hivatkozni, így függvényhíváskor is mutató kerül átadásra, érték szerint, éppcsak szerettem volna elkerülni a mutató szó használatát. Szemben például a C-vel, ahol teljes objektum adható át, érték szerint.
Egy szigorúan egyszeri értékadást követő nyelvben az örökölt hatókör és az argumentumoké esetén is érdektelen volna ebből a szempontból az átadás jellege, mert hiába direkt az elérés, ha csak olvasásra jogosít.
Nagyjából az az előnye a
Procedurális (imperatív) megközelítésnél a függvényeken belül a
global
vagystatic
kulcsszót kell használnod, hogy elérd a scope változóit, de az csak megegyezés kérdése, hogy mindent paraméterként adsz át, és onnantól kezdve ugyanott vagy, mint a funkcionális programozásnál, de használhatsz valódi változókat és ciklusokat, azaz átláthatóbb és gyorsabb lesz a programod.Az OOP ebből a szempontból tragédia, hisz minden objektumnak saját állapota lehet. Ha egy osztályban használsz
private
vagyprotected
változókat, a kód ismerete nélkül sohasem tudod megmondani biztosan, hogy egy metódust ugyanazokkal a paraméterekkel meghívva (ugyanazon publikus adatokat tartalmazó objektumon) ugyanazt az eredményt kapod-e vissza. Innentől kezdve nem teljesül az a kijelentés, hogy az objektum fekete dobozként használható, azaz ebből is csak az látszik, hogy az OOP elvei mennyire instabilak.global
kulcsszót használunk procedurálisan. Annyi különbséggel, hogy OOP-ben az injektálás után márthis
-szel kell hivatkozni rá, de ettől még ugyanúgy globális marad, és az alkalmazás állapotát határozza meg.Ha itt arra gondolsz, hogy
Igen, de szabályozva van, hogy hol injektáljuk be. Általában a példányosításnál csináljuk, amiért sokszor egy dependency injection container felelős. Szóval ha tudni akarjuk, hogy honnan jött valami, akkor csak megkeressük, hogy melyik container példányosította az objektumot, és ha esetleg az injektált példányváltozó is objektum, akkor legtöbbször azt is ugyanaz a container példányosította, szóval azt is hamar meg tudjuk találni. Ezzel szemben a procedurálisnál nehéz megtalálni, hogy a scope-ban hol kapott legutóbb értéket egy változó. Különösen rossz a helyzet, ha több helyen is írják. Mert nem követhető könnyen, hogy ezeknek mi az időrendje. Amit Ádám javasolt, hogy csak egyszer állítsunk be értéket a scope-ban, szóval legyen immutable, az sokat javít a helyzeten.
Ezek mind megegyezés
Az OOP-ban az objektumok állapotai jóval nagyobb problémát jelentenek, hisz ha most azt mondanád egy átlagos programozónak, hogy ezentúl ne használjon belső változókat, csak publikusakat, akkor úgy nézne rád, mint ti rám, amikor az OOP-t kritizálom.
Minden belső változó a program komplexitását növeli, és minél több van bennük, annál nagyobb a valószínűsége, hogy teszteletlen ágra fusson a program, mivel a készítője nem tud átgondolni minden interakciót, ami az objektumok között történik.
Ennél csak jobb lehet, ha nem használunk globális változókat, hanem mindent paraméterként adunk át, vagy megegyezés szerint ezeket csak függvénnyel írjuk, közvetlenül sosem.
Két észrevétel, ami el van
Pont ezért (na jó, ezért is)
Ő szerintem a testing pyramid-re gondolt. A unit test-eket még meg tudod írni mindet, az integrációs teszteknél viszont gondban leszel, mert nem tudsz minden útvonalat tesztelni. Legalábbis hagyományos webalkalmazásoknál. Katonainál gondolom rászánják a pénzt, és minden egyes if-else-t tesztelnek.
Szvsz a biztonságot nem az adja, hogy valami tesztelt, hanem az, ha tesztek alapján fejlesztjük. Így garantálható, hogy nem került bele olyan kód, amihez nincsen legalább valamilyen szintű teszt.
Hát ja. Gábor is eljutott idáig. :D (de mindjárt kimagyarázza, ne aggódj :D)
A unit test-eket még meg
Ami a 100%-os lefedettséget illeti, olyan valójában nincs. Se katonai, se orvosi, se pénzügyi stb területen. Mi azt gondolom nagyon szigorú tesztek, lefedettség és processek alapján fejlesztünk, de 'csodák' mindig vannak :) Mindenre nem lehet felkészíteni egy programot.
Pont ezért (na jó, ezért is)
Ez a bekezdésed nekem sántít,
Mit rontok el?
[0, 1, 2, 3]
-at ad vissza.Az utolsó blokkban a
divide
definíciója helyes?Hiba
compose
függvénybe került egy hiba.A
divide
jó, bár nálam mégdivider
-nek hívták, ezért is volt felcserélve az argumentum.Most jó