A wrapper function (Node.js – 1.)

A kódbazis.hu (Bolgár Máté) Node.js tutorial alapján.

A Node.js egy futtató környezet, ami lehetőséget nyújt JavaScriptben írt programok futtatására. JavaScriptet leginkább kliens oldalon szoktak használni a böngészőn keresztül. A Google Chrome böngésző JavaScriptet futtató motorja a V8 engine, aminek a fejlesztése 2006-ban kezdődött. Neve játékos utalás a V8 benzinmotorok teljesítményére (a V8 motorokban 8 henger van két sorban elhelyezve, melyek ‘V’ betűt formázó szöget zárnak be). A Node.js is ezt a motort használja némi módosítással, továbbá beépített könyvtárakat tartalmaz. Alapvetően webszerverek készítésére hozták létre.

Nincs GUI-ja, csak CLI-ből használható. Használata adott mappán megnyitott parancssorból vagy VS Code termináljából: készítünk egy pl. index.js fájlt, majd beírjuk a parancssorba, hogy node index.js, akkor a node lefuttatja az index.js fájlt.

A wrapper function

A a parancssorban a node paranccsal lefuttatott kód tkp. egy wrapper function-be van csomagolva, azaz mintha a kód körül lenne véve ezzel a wrapper function-nel, s futtatáskor ez hívódna meg. Ez a function kap paramétereket, amelyeket a forráskódban tudunk használni. Ezek: __dirname, __filename; module, require, exports, ezeket ki is lehet logolni.

  • __dirname: a forrásfájl operációs rendszeren belüli abszolút útvonala
  • __filename: ugyanez, csak még hozzáfűzve a fájl neve
  • module, require, exports: fájlok között funkcionalitás megosztása; ha van egy másik fájlunk, s abban egy function, amit szeretnénk megosztani, akkor a module object export kulcsa alá kell bekötni a megosztani kívánt adatot, ami leginkább object vagy function szokott lenni. Pl.: a fájl neve legyen calculator.js.
function add(num1,num2){
     return num1+num2
}

module.exports = add
  • require: Innentől kezdve ez a function elérhető a többi fájl részére is. Abban a fájlban, ahova meg akarjuk hívni a függvényt, ott a require() függvényben kell megadni a behúzni kívánt fájl relatív útvonalát. A fájlkiterjesztés JavaScript fájlok esetén (is) elhagyható, ill. egészen pontosan, ha nem adunk meg fájlkiterjesztést, akkor a node.js először a .js, majd a .json, majd a .node fájlok között fogja keresni az adott fájlt. Ez keveredést okozhat, így jobb odaírni a fájlkiterjesztéseket.
const add = require('./calculator.js')
console.log(add(4,5)  //9
  • require.resolve: szerver oldalon gyakrabban van szükség egy modul elérési útvonalára. Ez az utasítás nem húzza be a függvényt, így azt nem tudjuk használni, csak a függvény elérési útvonalával tér vissza. Pl.:
const add = require.resolve('./calculator.js');
console.log(add) //C:\Users\Kevin\Desktop\new_map\calculator.js

A Node.js-ben tehát a module.exports utasítással exportálunk és a require utasítással importálunk. Egy fájlból ezen a módon csak egy adatot tudunk exportálni, s importáláskor az az adat lesz behúzva. Egy fájlon belül több module.exports=… utasítás esetén az utolsó lesz érvényes. Hogy hogyan lehet másként egy modulból több adatot exportálni, arra később visszatérünk.

Ez a Node.js esetén rákényszerít a modular code technikára, azaz az egy függvény – egy modul eljárásra.

Viszont a require függvény és a module object a Node.js-en belül globális scope-pal rendelkezik, így nincs szükség a require(‘require’), ill. a require(‘module’) utasításokra.

Az exports és a module.exports

Az adatok exportálása tehát a module.export utasítással történik, ami tkp. a module object egy export nevű kulcsa, s ez alá írunk be valamit. Ugyanakkor a Node.js-en belül az export egy olyan object, amelyet deklarálás nélkül használhatunk. Pl. deklarálás nélkül beírhatjuk egy fájlba:

exports.kulcs="hello"
console.log(exports.kulcs)  // hello

Azaz a wrapper function exports paramétere és a modules.export két különböző dolog!

Mi a module?

[Irodalom: developpaper.com] Írok két JS fájlt, az index1.js-et és az index2.js-t, s mindkettőt a Desktopon helyezem el.

//index1.js

function add(num1,num2){
console.log(num1,num2);
return num1+num2};
module.exports=add;
console.log(module);
//index2.js

add = require('./index1.js');
console.log(add(2,5));
console.log(module);

Az index1.js module-ja ez lesz:

C:\Users\Kevin\Desktop>node index1.js
 Module {
  id: '.',
  path: 'C:\Users\Kevin\Desktop',
  exports: [Function: add],
  parent: null,
  filename: 'C:\Users\Kevin\Desktop\index1.js',
  loaded: false,
  children: [],
  paths: [
    'C:\Users\Kevin\Desktop\node_modules',
    'C:\Users\Kevin\node_modules',
    'C:\Users\node_modules',
    'C:\node_modules'
 ]
}

Az index2.js module-ja ez lesz:

Module {
 id: '.',
 path: 'C:\Users\Kevin\Desktop',
 exports: {},
 parent: null,
 filename: 'C:\Users\Kevin\Desktop\index2.js',
 loaded: false,
 children: [
   Module {
     id: 'C:\Users\Kevin\Desktop\index1.js',
     path: 'C:\Users\Kevin\Desktop',
     exports: [Function: add],
     parent: [Circular *1],
     filename: 'C:\Users\Kevin\Desktop\index1.js',
     loaded: true,
     children: [],
     paths: [Array]
   }
 ],
paths: [
 'C:\Users\Kevin\Desktop\node_modules',
 'C:\Users\Kevin\node_modules',
 'C:\Users\node_modules',
 'C:\node_modules'
 ]
}
  • id attributum: a fájl abszolút elérési útvonala, ennek az alapján azonosítja a node.js a fájlt.
  • paths attributum: láthatóan egy halom – létező és nem létező – abszlút útvoval az operációs rendszerel belül. Az index2.js a require függvénnyel behúzza az index1.js-t, amelynek az elérési útvonalát adjuk meg a függvény paramétereként: add = require(‘./index1.js’). Ehelyett írhatjuk ezt is: add = require(‘index1.js’), azaz itt csak a file nevét adtuk meg, nem adtunk hozzá elérési útvonalat. Ekkor a node.js a paths attributum alatt megadott elérési útvonalakon próbálja megtalálni a fájlunkat. S így már nem fogja megtalálni, mivel a C:\Users\Kevin\Desktop nem szerepel az útvonalak között. De ha készítünk egy node_modules mappát a Desktopon, s abba helyezzük el az index1.js fájlt, akkor meg fogja találni, mivel a C:\Users\Kevin\Desktop\node_modules szerepel a paths útvonalai között.
  • children, parent: az index2.js a require eljárással behúzza az index1.js-t, így az szerepel a children tömbjében. Az már nem húz be semmit, ezért annak nincs gyereke. A parent-nál viszont azt látjuk, hogy [Circular *1], mivel itt egy körkörös – önmagába visszatérő – függőségről van szó, hiszen az index1.js-t az index1.js-ből húztuk be, így saját maga a szülöje.
  • export: a module object export kulcsa alá kerül be az, amit kiexportáltunk a fájlból.
  • loaded: jelzi, hogy a module ba van-e töltve. Akkor lesz az értéke true, ha a module teljesen be lett töltve. Így a fentiek mellett a node index2.js utasításra a kiírt module-ban azt látjuk, hogy az index2.js-nál ez false, mivel a kiíratáskor a fájl még fut, de az index1.js-re true, mivel a kiíratáskor az már be lett töltve a require utasítással mindjárt a fájl elején.

A kód futtatása a következőképpen történik (kép forrása):

  • resolving: a célmodul megtalálása és az abszolút út generálása
  • loading: annak meghatározása, hogy a modul JS, JSON vagy Node fájl-e
  • wrapping: becsomagolás privát scope-ba
  • evaluating: futtatás
  • caching: cache-elés, hogy későbbi használatnál ne kelljen a fenti lépéseket megismételni

Wrapping

A node.js tehát nem direkt módon futtatja az adott js kódot, hanem előtte becsomagolja a kódot egy wrapper függvénybe. Ez a function így néz ki (nodejs.org dokumentációja):

(function (exports, require, module, __filename, __dirname) {
  //module code
});

Ezzel a következő dolgok valósulnak meg:

  • Mivel itt függvény scope-ról van szó, így a modulon belül használt var, let, const kulcsszavakkal deklarált változók scope-ja a modul lesz, így azok nem lesznek globálisan elérhető változók.
  • Minden modulnak megvan a saját wrapper függvénye, s a __dirname, __filename; module, require, exports változók minden modul esetében lokálisak.

Az index2.js fájlon belül console.log(arguments) utasítással kiírathatjuk a wrapper function paramétereit:

[Arguments] {
 '0': {},
 '1': [Function: require] {
   resolve: [Function: resolve] { paths: [Function: paths] },
   main: Module {
     id: '.',
     path: 'C:\Users\Kevin\Desktop',
     exports: {},
     parent: null,
     filename: 'C:\Users\Kevin\Desktop\index2.js',
     loaded: false,
     children: [Array],
     paths: [Array]
   },
   extensions: [Object: null prototype] {
     '.js': [Function (anonymous)],
     '.json': [Function (anonymous)],
     '.node': [Function (anonymous)]
   },
   cache: [Object: null prototype] {
     'C:\Users\Kevin\Desktop\index2.js': [Module],
     'C:\Users\Kevin\Desktop\node_modules\index1.js': [Module]
   }
 },
 '2': Module {
   id: '.',
   path: 'C:\Users\Kevin\Desktop',
   exports: {},
   parent: null,
   filename: 'C:\Users\Kevin\Desktop\index2.js',
   loaded: false,
   children: [ [Module] ],
   paths: [
     'C:\Users\Kevin\Desktop\node_modules',
     'C:\Users\Kevin\node_modules',
     'C:\Users\node_modules',
     'C:\node_modules'
   ]
 },
 '3': 'C:\Users\Kevin\Desktop\index2.js',
 '4': 'C:\Users\Kevin\Desktop'
}

Az exportálás három módja

(Irodalom: Srishti Gupta – medium.com) A legegyszerűbb módra fenntebb láttunk példát:

function add(num1,num2){
return num1+num2
}

module.exports=add

A module.export tkp így néz ki (kép forrása, mondjuk a helyesírás-ellenőrőt kikapcsolhatta volnaSrishti Gupta):

S mivel a module.export eredetileg egy object, ezért használhatunk rajta kulcsokat, azaz különböző kulcsok alatt különböző adatokat adhatunk meg, majd a require utasítással ezt az object-et kapjuk meg, aminek hivatkozhatunk a kulcsaira.

//index1.js

const add = function(num1,num2){
console.log(num1+num2);
return num1+num2};
module.exports.add=add;

function mult(num1,num2){
console.log(num1num2);
return num1num2};
module.exports.mult=mult;

//index2.js

const add = require('./index1.js').add;
add(2,5)
const mult = require('./index1.js').mult;
mult(2,5)

//7
//10

A másik megoldás ugyanerre épül, csak ott az object-et máshogy adjuk meg:

const add = function(num1,num2){
console.log(num1+num2);
return num1+num2}

function mult(num1,num2){
console.log(num1num2);
return num1num2}

module.exports = {add:add, mult:mult}

(Még tömörebben az uolsó sor: module.exports = {add, mult}

require.cache

Egy fájlon belül a console.log(require) utasítással kiírathatjuk a require objectumot. Ekkor az objectumon belül látni fogunk egy chache kulcsot, ami alatt az eddig betöltött modulok lesznek eltárolva, s így az ismételt modulhívás esetén tkp. csak cache-elés történik.

Rxjs – Observable

Mi ez?

Az Observable tkp. olyan mint a Promise, de nem része a JavaScript szabványnak. Az Observable szó azt jelenti, hogy ‘megfigyelhető‘, azaz egy olyan objektum, amelyik folyamatosan változik és meg lehet figyelni, és az alkalmazás különböző pontjain különböző megfigyelők iratkozhatnak fel rá.

  • lehet több megfigyelő
  • az esemény bekövetkeztéről mindenki könnyen értesülhet
  • az eseményeket mint adatfolyamot kezeljük
  • lehet manipulálni (pl. filter, map)
  • mellékhatásokat tudunk vele kiváltani (pl. kiiratjuk az értéket a konzolra)

A reaktív programozás

A következő szövegek forrása: Horváth Győző – Móger Tibor László: Webes környezet aszinkron kódjainak elemzése, refaktorálása

“A program állapotát deklaratívan az adatfolyamok közötti kapcsolatokkal írjuk le, így ha egy adatfolyamon valamilyen változás következik be, akkor az az egész rendszeren végigterjedve változásokat idézhet elő, ha a megfelelő függőségek léteznek”. Azaz attól reaktív, hogy ha az adatfolyamon változás következik be, akkor az reakciók sorozatán terjed tovább az egész rendszeren.

A reaktív programozás egy programozási paradigma. “Az elve, hogy az adatokat egyszerű változók helyett adatfolyamokban tároljuk, amelyek időben változhatnak. … Egy gyakran használt példa a reaktív programozás szemléltetésére a dupla kattintások kezelése. A dupla kattintást definiálhatjuk úgy, hogy két kattintás között, közel ugyanazon a pozíción kevesebb, mint fél másodperc telik el. Egy dupla kattintás esetén szeretnénk a duplán kattintott elem pozícióját kiírni a képernyőre. Egy nem reaktív megoldás pontosan az ezt megvalósító kódot írná le egy eseménykezelőben. Minden „első” kattintás esetén eltárolná az aktuális eseményt, beállítana egy időzítőt, ami a fél másodperc letelte után eltávolítaná az „első” eseményt. Ha még azelőtt beérkezik egy újabb esemény, hogy ez megtörténne, akkor az új kattintás már „második” kattintásnak minősül, tehát ez egy dupla klikk volt, ekkor pedig meghívja a képernyőre kiíró függvényt. Ez a megoldás nem túl reaktív, például adatfolyamok egyáltalán nem jelennek meg benne. “

“Egy reaktívabb megoldás lenne, ha a dupla kattintásokat egy adatfolyamként definiálnánk, és erre feliratkozva írnánk ki a képernyőre az adatokat. … Erre egy jó leíró eszköz a Reactive extensions. Ebben az eszközben a fenti példa valahogy úgy működne, hogy létezik egy adatfolyamunk a kattintásokról. Erre az adatfolyamra rárakunk egy olyan megfigyelőt, aki minden „első” esemény esetén bufferelni kezdi az adott időablakon belül beérkező eseményeket, majd egy olyan adatfolyamként testesül meg, ami az első események után lejárt időablak végén bocsát ki magából adatokat, és ebben az időablakban megjelent eseményeket buffereli. A következő megfigyelőnk megszámolná, hogy hány esemény lett bufferelve, és ezt bocsátaná ki magából. További megfigyelőkkel pedig leszűrnénk ezt az adatfolyamot a kettőnél több kattintást tartalmazó időablakokra, és az egymáshoz közeli kattintásokra. A végül előállt adatfolyamunkra pedig egy imperatív feliratkozáson keresztül létrehozunk egy reakciót, ami kiírja a felületre a kattintott elem pozícióját. Ez rendkívül körülményesnek tűnhet, de a megfelelő absztrakciók jelenlétében egy jól olvasható deklaratív megoldást ad.”

Reactive extensions

“A reaktív programozásnak az egyik implementációja az úgynevezett Reactive extensions (Rx), amely egy nyelveket átívelő interfész annak a kifejezésére, hogy hogyan lehet jól reaktív programokat leírni. Ez egy push alapú, szükség esetén aszinkron, lusta implementáció. Most nézzük meg jobban, hogy mit értünk ez alatt.

  • A push alapú azt jelenti, hogy az egy adatfolyamra feliratkozók akkor kapnak adatokat, ha az eredeti adatfolyamon megjelenik egy adat. Ennek ellentéte a pull alapú lehetne, ami viszont azt jelentené, hogy az eredeti adatfolyamnak akkor kell adatot szolgáltatnia, amikor a feliratkozó azt kéri tőle.
  • A szükség esetén aszinkron alatt azt kell érteni, hogy ha folyamokon elhelyezett adatfolyamként megfigyelhető feliratkozóknak áll rendelkezésre adat, akkor azt szinkron dolgozzák fel, de ha nincs ilyen, akkor várakoznak.
  • A lustaság alatt itt azt kell érteni, hogy ha nincs imperatív feliratkozója egy adatfolyamnak, akkor hiába push alapú a rendszer, az alul levő adatfolyamon nem jelennek meg értékek, vagy azokat nem dolgozzuk fel. Ez alól majd kivételt jelentenek a hot streamek, ahol az adatfolyam olyan, mintha lenne imperatív feliratkozója, de ilyen valójában nincs.”

A tulajdonságokat követően nézzük az Rx építőköveit:

  • Observable: Ezek felelnek meg az adatfolyamoknak.
  • operátorok: Az operátorok segítségével adatfolyamok közötti transzformációkat tudunk megvalósítani. Operátor lehet például egy adatfolyam megszűrése, megszorzása kettővel, időablakokra osztása, késleltetése, aggregálása. Az operátorok általában egy observable-t várnak paraméterül, és egy observable-t adnak vissza. Ennek előnye, hogy az operátorokat tudjuk láncolni, így kényelmesen egy bonyolultabb adatfolyam átalakítást le tudunk írni operátorok kompozíciójaként.
  • Subscriber/Subscription: Egy observable-re feliratkozhatunk a subscribe metódusuk segítségével. Ha ez egy cold observable, akkor ezzel tudjuk aktiválni a működését. Az adatfolyamok utolsó láncszemeként megjelenő adatfolyamokra itt lehet mellékhatásokat elhelyezni. Egy feliratkozás létrehoz egy Subscription objektumot, ami életben tudja tartani az adatfolyamot, ezért ha az adatfolyam nem fejeződött be akkor, amikor már nincs rá szükségünk, akkor ezeket pusztítsuk el.”

RxJS

“A JavaScript világban az RxJS könyvtár adja ezeket az Rx által leírt eszközöket. … Az RxJS nem szervesen a JavaScript nyelv része, habár voltak erőfeszítések arra,
hogy az alapjait támogassák a nyelv szintjén a futtatókörnyezetek. Ennek ellenére
ez is egy eszköz az aszinkronitás kezelésére.”

(Forrás – Horváth Győző – Móger Tibor László: Webes környezet aszinkron kódjainak elemzése, refaktorálása)

RxJS használata – operátorok

RxJS betöltése CDN-ről: <script src=”https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js”></script>

Példakód npmjs.com-ról:

const { range } = rxjs;
const { map, filter } = rxjs.operators;

range(1, 200)
  .pipe(
    filter(x => x % 2 === 1),
    map(x => x + x)
  )
  .subscribe(x => console.log(x));

Kétféle operátor van. A Creation Operators, amelyek egy új Observable-t hoznak létre, ill. a Pipeable Operators, amelyek tkp. – az observableInstance.pipe(operator()) szintaxissal használható – metódusok, s módosítják a Observablet, de nem az eredeti Observable-t írják felül, hanem egy új Observable-vel térnek vissza.

A fenti példában a range(start: number, count: number) operátor fogja létrehozni az Observable-t, ami tkp. maga a stream, az adatfolyam. Paraméterei:

  • start: ez lesz a számsorozat első egész értéke, s innentől kezdve növekednek a kibocsátott , amíg el nem érik a count-ban megadott darabszámot. Opcionális, default értéke a nulla.
  • count: ennyi értéket fog kibocsátani

A példában pedig két pineaple operátor a filter és a map, amelyek ugyanúgy működnek, mint az azonos nevű tömb metódusok. A filter itt leválogatja a páratlan számokat, a map pedig mindegyiket megduplázza.

const { of } = rxjs;
const { map } = rxjs.operators;

of(1, 2, 3)
  .pipe(map((x) => x * x))
  .subscribe((v) => console.log(`value: ${v}`));

Ebben a példában (rxjs.dev) az of(1,2,3) létrehozza azt az Observablet, amely az 1, 2, 3 értékeket bocsátja ki egymás után. A map metódus mindegyiknek veszi a négyzetét, majd a subscribe operátorral feliratkozunk az utoljára kibocsátott stream-re, s ezt kapjuk a konzolon: 1, 4, 9.

Operátorokról részletesen: rxjs.dev, ill. javatpoint.com.

A lebegőpontos számábrázolás – avagy: amikor a fal adja a másikat

Az alapok

Az alapokat nem írom le. Rengeteg helyen elmagyarázzák. Jómagam pl. innen értettem meg – bevallom, nagyon nehezen ment, mert kicsit idegen a mindennapok világától a lebegőpontos számábrázolás szemlélete, s ha szükség van rá, bármikor belezavarodok magam is: https://gyires.inf.unideb.hu/GyBITT/31/ch05s05.html

A lényeg az, hogy a JavaScript a 64 bit hosszúságú, ún. dupla pontosságú lebegőpontos számábrázolást (double precision floating point format) használja, ahol a felosztás: 1 bit előjel – 11 bit kitevő – 53 bit mantissza. Viszont 1 + 11 + 53 = 65, ami 1-gyel több, mint 64. A mantissza ui. mindig egy 1-es rejtett bittel kezdődik, amit nem írunk ki. A kitevők ábrázolása viszont csak 1024-ig lehetséges (210) a feszített előjeles ábrázolás miatt.

Speciális számok

ElőjelKarakterisztikaMantissza
denormalizált szám ±0tetszőleges szám ≠ 0
nulla ±00
végtelen ±111…1110
NaN ±111…111tetszőleges szám ≠ 0
  • Látható, hogy két nulla van, pozitív és negatív nulla, ahogyan a végtelenből (Infinity) is.
  • A +0 és a -0 között egyetlen különbség van, ha ezzel osztunk egy nem nulla véges számot: 1/+0 == +Infinity, ellenben 1/-0 == -Infinity. Minden más esetben pontosan ugyanúgy viselkednek, és -0 === +0.
  • A NaN a ‘Not a Number‘ rövidítése, olyan értéket takar, amit akkor kapunk, ha a számolás kivezet a (valós) számkörből, pl. Math.sqrt(-1) vagy 0/0. A NaN semmivel sem – így önmagával sem – egyenlő.
  • A denormalizált számról később lesz szó.
  • Lényeges, hogy ezek a ‘számok’ a JavaScriptben number típusú primitívek, azaz a typeof-juk number, tehát számnak minősülnek, mint ahogyan az ábrázolásuk is lebegőpontosan történik.

Példák az egész számok ábrázolására

Ezen az oldalon tudunk átalakítani decimális alakot lebegőpontossá. Újratöltéshez használjuk a dec to bin 64bit gombot. (Update: ez a konverter néha hibázik, pl. 0.1-re.)

232-1 = 4 294 967 295(10) = 1111 1111 1111 1111 1111 1111 1111 1111(2) = 1.1111 1111 1111 1111 1111 1111 1111 111(2) × 231 = 0 -100 0001 1110 – 1111111111111111111111111111111000000000000000000000 – a szám egész része 32 bit hosszúságú, igy az exponens 31 lesz, amiből a kettes számrendszerben a feszített előjeles ábrázolás miatt 1023+31 = 1054 lesz, azaz: 100 0001 1110(2)  = 1024+0+0+0+0+0+16+8+4+2+0 = 1054; a mantisszából az első 1-es számjegy nem látszik, az lesz a rejtett bit; a kettedespontot követő kettedes jegyeket feltöltik az 52 bitnyi helyre úgy, hogy az utolsó egyest követő helyekre 0-kat írnak.

232 = 4 294 967 296(10) =1 0000 0000 0000 0000 0000 0000 0000 0000(2) = 1.0000 0000 0000 0000 0000 0000 0000 0001(2) × 232 = 0 -100 0001 1111 – 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 – a szám egész része itt 33 bit, így a kitevő 32 lesz, ebből a kettes számrendszerben a feszített előjeles ábrázolás miatt 1023+32 = 1025 lesz, azaz 100 0001 1111(2) = 1024 +0+0+0+0+0+016+8+4+2+1 = 1025; a mantisszában csak egy darab 1-es lenne, az első, de az lesz a rejtett bit, így csak 52 nulla sorakozik egymás után.

232+1 = 4 294 967 297(10) = 1 0000 0000 0000 0000 0000 0000 0000 0001(2) = 1.0000 0000 0000 0000 0000 0000 0000 0001(2) × 232 = 0 -10000011111 – 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0000 0000 – az 1-es számjegy egy 33 számjegyű szám utolsó számjegye. A normál alakban az lesz a 32. kettedes jegy a kettedes pont után, így a lebegőpontos ábrázolás mantisszájában ez lesz a 32. számjegy.

232+2 = 4 294 967 298(10)  = 1 0000 0000 0000 0000 0000 0000 0000 0010(2) = 1.0000 0000 0000 0000 0000 0000 0000 0010(2) × 232 = 0 – 100 0001 1111 – 0000 0000 0000 0000 0000 0000 0000 0010 0000 0000 0000 0000 0000

232+3 = 4 294 967 299(10)  = 1 0000 0000 0000 0000 0000 0000 0000 0011(2) = 1.0000 0000 0000 0000 0000 0000 0000 0011(2) × 232 = 0 -10000011111 – 0000000000000000000000000000001100000000000000000000


MAX_SAFE_INTEGER

A Number objectum értéke ezen a kulcson a Number.MAX_SAFE_INTEGER = 9007199254740991, amely a legnagyobb biztonságosan ábrázolható egész szám a 64 bites lebegőpontos számábrázolás esetén, ami nem más, mint a 253-1.

A mantissza 52 látható és 1 rejtett bitből, azaz összesen 53 bitből áll. 53 biten pedig összesen 253 darab szám ábrázolható, s mivel a sort a 0-val kezdjuk, így a legnagyobb 53 bites szám a 253 -1 (= 9007199254740991) lesz, ami így néz ki: 0 -10000110011 – 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111, s ez lesz az a bizonyos nagy szám.

A Number.MIN_SAFE_INTEGER pedig ennek az ellentettje, a legkisebb biztonságosan ábrázolható (negatív) egész szám.

Élet a MAX_SAFE_INTEGER-en túl

Nézzük meg, mi történik, ha hozzáadok 1-et! Ekkor 253 = 9007199254740992, ami kettes számrendszerben felírt normál alakban: 1.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0(2) × 253. A mantisszában csak 52 kettedes jegy lehet, így az utolsó – az 53. – kettedes jegy itt túlcsordulással eltűnik a lebegőpontos felírásban: 0 -10000110100 – 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

Adjunk hozzá ismét 1-et! Ekkor 253+1 = 9007199254740993, ami 1.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1(2) × 253, ahol túlcsordulás miatt az utolsó kettedes számjegy eltűnik: 0 -10000110100 – 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000, így ugyanazt a számot kaptuk meg, mint az előbb. Ezért a JavaScriptben 9007199254740992 + 1 = 9007199254740992, azaz a 2**53==2**53+1 logikai kifejezés a true értéket fogja kapni, azaz 9007199254740992 és 9007199254740993 ugyanaz az egész szám. Sőt! 9007199254740993 + 1 = 9007199254740992.

Beugrató kvízkérdésnek sem rossz:

          Mennyi 9007199254740993 + 1 a JavaScriptben?

          A. 9007199254740991
          B. 9007199254740992
          C. 9007199254740993
          D. egyik sem

A határszám nagyságrendjét úgy tudjuk megjegyezni, hogy 253 -1 közelítve: 250 = (210)5 = 10245, ami közelítve: 10005, ez pedig 3×5, azaz 15 db. nulla, s maga a szám pedig: 9.007199254740991e+15, azaz nagyságrendileg 9×1015 .

A következő szám a 9007199254740994, ami 1.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0(2) × 253, ami lebegőpontosan: 0 -10000110100 – 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001. Ez viszont már egy létező szám lesz, ahol a 9007199254740992-t a 9007199254740994 követi és 9007199254740992 + 2 = 9007199254740994.

Ugyanígy a következő 9007199254740995 az 1.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1(2) × 253, ami 0 – 10000110100 – 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 – s ez ugyanaz, mint az előző – 4-re végződő – szám. Ha viszont beírjuk a konzolba, hogy 9007199254740995 és nyomunk egy entert, akkor azt kapjuk, hogy 9007199254740996.

9007199254740996 viszont a 1.0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 0(2) × 253, ami 0 – 10000110100 – 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010.

Az előbb az eggyel alacsonyabb, most viszont az eggyel magasabb értékkel lesz egyenlő az adott – egyébként nem tárolt szám. Azt várnánk, hogy itt a 9007199254740995-ös szám a lebegőpontos felírásának megfelelő 9007199254740994-es számon tárolódik el, helyette viszont az – eltárolásában 1-gyel nagyobb mantisszát tartalmazó – 9007199254740996-os számon tárolódik el. Miért? Erre egy stackoverflow.com fórumon találtam választ:

>> When the number can’t be represented by the format, it is rounded to the nearest representable value; if it is exactly halfway between two such values, it is rounded to the nearest “even” number, which is the one with a 0 in the last place of the significand. <<

Azaz: amikor egy számot nem lehet a lebegőpontos formában ábrázolni, akkor a számot kerekítjük a legközelebbi ábrázolható szám értékére. Ha viszont a szám pont a kettő között van félúton, akkor a legközelebbi (binárisan) ‘páros’ számra kerekítjük, arra, amelyiknek az utolsó (bináris) számjegye a 0.

Azaz az el nem tárolt 9007199254740995-ös szám egyenlő távolságra van az eltárolt 9007199254740994-es és a 9007199254740996-os számoktól, így az az eltárolt szám fogja reprezentálni, amelyik a tárolt bináris formátumában páros, azaz a lebegőpontos ábrázolásban a mantisszája 0-ra végződik. (Tehát a kettes számrendszerben legyen páros, ne a tizes számbendszerben!)

A szám tehát, amit kerekítenünk kell 0001 1(2) -re végződik. Két szomszédja, amelyek között pont félúton van, az a páros 0010(2) végű és a páratlan 0001(2) végű szám. Ezek közül a páros szomszédjára kerekítünk, ez pedig a 9007199254740996.

Továbbá látható, hogy itt mindig két lépésenként növekszik 1-gyel az utolsó látható számjegy, azaz a növekvés így néz ki: 000, 001, 010, 011,100,101,110,111 …, s emiatt az ábrázolható számok ebben a nagyságrendben (253) kettesével növekednek.

Elvileg tehát mondjuk egy 255 nagyságrendű szám esetében 55-52=3, azaz az utolsó három bit marad le, amin a következő számok ábrázolhatóak: 000(2) = 0 -tól 111(2) =4 + 2 + 1 = 7 -ig, azaz nyolcanként kell ugrálni a számoknak. Ami ugyan igaz is, de nem úgy, ahogyan mi azt várnánk.

Nézzük tehát 255 = 10000000000000000000000000000000000000000000000000000000 -től, ahol pontosan 55 db nulla van.

Exponenciális alakValódi érték JavaScript érték
25536,028,797,018,963,96836 028 797 018 963 970
255 + 836,028,797,018,963,97636 028 797 018 963 976
255 + 1636,028,797,018,963,98436 028 797 018 963 980
255 + 2436,028,797,018,963,99236 028 797 018 963 990
255 + 3236,028,797,018,964,00036 028 797 018 964 000
255 + 4036,028,797,018,964,00836 028 797 018 964 010

Ennek az okát csak valószínűsíteni tudom, ui. úgy tűnik, hogy itt egy újabb szabály jön be, s a kerekítések úgy történnek, hogy – amennyiben lehetséges – a szám csak a tizes helyiértéken ugorjon egyet, s az egyesek helyén pedig 0 legyen. Ezt akár ellenőrizhetjük is mondjuk a 264 nagyságrendű számokkal, ahol 64 – 52 = 12 alapján, mivel 212 = 4096, a számok már három nullára lesznek kerekítve. S úgy tűnik, hogy ez így is van.

Exponenciális alakValódi értékJavaScript érték
26418,446,744,073,709,551,61618 446 744 073 709 552 000
264 + 21218,446,744,073,709,555,71218 446 744 073 709 556 000
264 + 2×21218,446,744,073,709,559,80818 446 744 073 709 560 000

MAX_VALUE

A kitevő – a szám karakterisztikája – 11 bites. Mivel a kitevőnek is van előjele, azt is ábrázolni kell, ezt azonban nem úgy teszik, hogy veszik itt is az első bitet, s ott helyezik el az előjelet, hanem veszik az 11 1111 1111(2) = 210 – 1= 1023 konstanst, s ehhez adják hozzá a kitevőt. Azaz a 0 kitevőt az 1023-as szám fogja tárolni, az 52-es kitevőt az 1023 + 52 = 1075 szám, a -20-as kitevőt pedig az 1023-20 = 1003-as szám tárolja.

A karakterisztika legnagyobb értéke a 111 1111 1111(2) = 211 – 1 = 2047 lehet, s mivel 2047 – 1023 = 1024, így elvileg a legnagyobb kitevő az 1024, a legkisebb kitevő pedig a -1023 lenne. Azonban:

  • Az 111 1111 1111 karakterisztika a végtelennek és a NaN-nak van fenntartva.
  • A 000 0000 0000 karakterisztika pedig a nullának és a denormalizált számoknak.

Így a legnagyobb karakterisztika az 1023, a legkisebb pedig a -1022. Azaz a legnagyobb ábrázolható szám lebegőpontos alakja: 0 11111111110 1111111111111111111111111111111111111111111111111111, aminek a decimális alakja: 1.7976931348623157e+308 – azaz ez egy 308 jegyű szám, aminek 16 értékes jegye van. Ezt a számot a Number objektum a MAX_VALUE kulcs alatt tárolja.

A legkisebb ábrázolható szám lebegőpontos alakja: 0 00000000001 000000000000000000000000000000000000000000000000000 – itt a feszítetten ábrázolt karakterisztika: 1, ami alapján az ábrázolt kitevő az a szám, amit 1023-hoz kell hozzáadnunk ahhoz, hogy 1-et kapjunk, ez pedig az 1022. Ne felejtsük el, hogy a mantissza elején a rejtett bitben ott van egy 1-es, így az ábrázolt szám az 1×2-1022 = 2.2250738585072014e-308.

Egy 1996-os JavaScript Reference Specification tanúsága szerint: “MIN_VALUE : The smallest number representable in JavaScript, 2.2250738585072014e-308.” Ugyanakkor, ha beírjuk a konzolra, hogy Number.MIN_VALUE, akkor ezt kapjuk: 5e-324. Ennek a magyarázata a developer.mozilla.org oldalán:

>> Number.MIN_VALUE is the smallest positive number (not the most negative number) that can be represented within float precision — in other words, the number closest to 0. That’s approximately 5E-324. The ECMAScript spec doesn’t define a precise value that implementations are required to support — instead the spec says, “must be the smallest non-zero positive value that can actually be represented by the implementation”. But in practice, its precise value in browsers and in Node.js is 2^-1074. <<

A mantissza 53 bites, így 253 – 1 számot képes tárolni. A legkisebb még lengőpontosan ábrázolható szám a 1×2-1022, ahol a feszített exponens 1. Viszont ha a teljes exponens részt 0-ra vesszük a lebegőpontos ábrázolásban úgy, hogy a mantissza nem nulla, akkor még mindig tudunk ábrázolni 252 -1 értéket, s ezek lesznek a denormalizált számok. A JavaScript így a denormalizált számokkal ábrázolja a 2-1022-nál kisebb számokat, s mivel 1022 + 52 = 1074, ezért a 2-1074 lesz a legkisebb ábrázolható szám. Ennek a lebegőpontos alakja: 0 – 000 0000 0000 – 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001

EPSILON

A Number.EPSILON azt az értéket tárolja, amely az 1 és a legkisebb 1-nél nagyobb lebegőpontos szám közötti különbséget tárolja. Mivel a mantissza 52 bit hosszúságú, ezért az értéke 2-52 = 2.2204460492503130808472633361816E-16.

Kicsit részletesebben a Round-Half-Even (Banker’s Rounding) kerekítésről

A mindennapi életben Round-Half-Up kerekítést használjuk, azaz, ha egy érték pont félúton van, akkor azt felfelé kerekítjük. Pl. 5.5 ≈ 6. Képek forrása, ill. kerekítések további ismertetése: www.eetimes.com.

Viszont kérdés, hogy mit értünk az alatt, hogy ‘felfelé‘ kerekítünk. UI. ha 5.5 ≈ 6, akkor ‘jó érzéssel’ azt várnánk, hogy -5.5 ≈ -6 legyen, azonban ekkor valójában lefelé kerekítjük a negatív számot, hiszen a ‘felfelé‘ a számegyenesen mindig a jobb felé mutató irány. Ezért külön kerekítés a Round-Half-Up kerekítésen belül a szimmetrikus (-5.5 ≈ -6) és az asszimetrikus (-5.5 ≈ -5) kiterjesztés a negatív számok kerekítésére. A JavaScript Math objektuma ennek a kerekítésnek az asszimetrikus kiterjesztését használja: Math.round(-6.5) = -6; ugyanakkor a .toFixed() metódus pedig a szimmetrikus kiterjesztést: (-6.5).toFixed() a ‘-7’ stringet adja vissza.

kerekítés szimmetrikus kiterjesztése a negatív számokra
kerekítés aszimmetrikus kiterjesztése a negatív számokra

A Round-Half-Up kerekítésnél az a lehetséges probléma, hogy mivel félúton mindig felfelé kerekít, ezért minél többször használjuk egy adathalmazra, annál több torzulást eredményezhet az állandó felfelé kerekítés.

Nem túl életszerű példa és nem is ezt a torzulás illusztrálja, de pl. ha egyenként veszek meg 10 darab 104 forintos valamit, akkor összesen 1050 forintot fogok fizetni, mert minden egyes fizetésnél a pénztárgép felfelé kerekít. Míg, ha egyszerre veszem meg, akkor csak 1040 forintot fogok fizetni, mivel ekkor 10 db kerekítés helyett csak 1 kerekítésre lenne lehetőség.

Ezt úgy lehet megoldani, hogy, ha az érték pont félúton van, akkor néha felefelé, néha pedig lefelé kerekítünk. Erre kínál megoldást a Round-Half-Even kerekítés. Ennél a kerekítésnél ui. ha a kerekítendő érték pont középen van, akkor mindig a (legközelebbi) páros (even) szomszéd értékére kerekít. Pl. 5.5 ≈ 6, de 6.5 ≈ 6. Itt a negatív számokra való kiterjesztéssel nincsen probléma, ui. -5.5 ≈ -6 és -6.5 ≈ -6. Ezt a kiterjesztést ‘Bankers Rounding‘-nak is nevezik, ui. pénzügyi területen használják leginkább.

Lényeges, hogy ez a kerekítés a bináris számrendszerben ugyanúgy használatos, ott ugyanis párosnak tekintjük a 0-ra, páratlannak pedig az 1-re végződő bináris számot.

A 0.1 + 0.2 = 0.30000000000000004 bug a JavaScriptben

Ha beírjuk a konzolra, hogy 0.1 + 0.2 == 0.3, akkor ‘false‘ értéket fogunk kapni. Az ilyen típusú hibáknak a lebegőpontos számábrázolás az oka. A 0.1 ugyanis lebegőpontosan: 0 – 01111111011 – 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010. Láthatjuk, hogy míg a 0.1 a tizes számrendszerben egy véges tizedes tört, addig a kettes számrendszerben egy végtelen szakaszos tizedes tört. Az exponens 1023 – 1019 = 4, azaz a szám 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010(2)  = 1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 x 2-4

A 0.2 pedig: 0 – 01111111100 – 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010, ahol az exponens 1023 – 1020 = 3, azaz a szám 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010(2)  = 1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 x 2-3.

Mindkét szám esetén a lebegőpontos ábrázolás utolsó két bitje kerekített érték. Ennek a módjáról lentebb még szó lesz.

Mivel a két exponens különböző, ezért a 0.1-et 3-as exponensűvé alakítjuk: 0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101 0 x 2-3

   0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101 0
 + 1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010  
  10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111 0

Ezt a számot normalizáljuk: 1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 1 x 2-2. Viszont itt túlcsordulás történik, s a 0011 1(2) pont félúton van a 0011(2) és a 0100 (2) között, s mivel az a szabály, hogy ha a szám pont félúton van két ábrázolható szám között, akkor a páros kettedes jegyre végződő szomszédjához kerekítjük. Így a lebegőpontos alakja: 0 – 011 1111 1101 – 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100 lesz.

Ez a szám pedig decimálisan a 0.300 000 000 000 000 044 408 920 985 006 261 616 945 266 723 632 812 5, s ez 17 tizedes jeggyel ábrázolva: 0.30000000000000004.

Forrás: indepth.dev, stackoverflow.com, betterprogramming.pub

Egy megjegyzés: két különböző Decimal to Floating-Point konverter a 0.1-re az utolsó két bitre nézve két különböző értéket ad, ami esetleg bezavarhat.

a hibás: binary-system.base-conversion.ro 
0-01111111011-1001100110011001100110011001100110011001100110011001
a (szerintem) helyes: exploringbinary.com 
0 01111111011 1001100110011001100110011001100110011001100110011010

Úgy tűnik, a kerekítéssel van gond. Az első konverter nem kerekíti le az 1001 [1001 ...] végződést. Az 1001[1] kerekítése: mivel az 1001[1] pont a 1001[0] és az 1010[0] között van félúton, ezért a páros szomszédjára kerekít, ami a 1010[0].

A mantissza tehát csak véges számú számjegyet tud tárolni, ezért a lebegőpontos ábrázolás a végtelen szakaszos tizedes törteket csak közelített értékkel tudja tárolni, ezért – amire mindenhol felhívják a figyelmet

“az egyik legelső dolog volt, amit álmunkból felrázva is tudnunk kellett: LEBEGŐPONTOS SZÁMOKAT SOHA NEM VIZSGÁLUNK EGYENLŐSÉGGEL!

Az EPSILON használata

A developer.mozilla.org oldalon – meg még mindenhol – a következő eljárást javasolják annak eldöntésére, hogy egy művelet eredménye azonos-e egy adott értékkel:

0.1+0.2
0.30000000000000004
0.1+0.2==0.3
false
Math.abs(0.1+0.2-0.3) < Number.EPSILON
true

Viszont ezt az eljárást egyelőre ne vegyük készpénznek, ui. van vele egy kis probléma, amire rögtön vissza is térünk.

A számok között lévő közök

Mint láttuk, az epszilon az a szám, amely az 1 és az 1-nél nagyobb legkisebb szám közötti különbséget reprezentálja. Az értéke tkp. az a köz, aminél közelebb nem lehet egymáshoz két szomszédos tizedes tört a lebegőpontos ábrázolásban. Tehát ez egy köz, ami meghatározza, hogy mi az a távolság, aminél nem lehet közelebb két, egynél nagyobb szám. Fontos, hogy az ábrázolás ennél a köznél kisebb számokat is ki tud fejezni, azaz ne keverjük az EPSILON és a MIN_VALUE fogalmát. Nézzük meg még egyszer, hogy mi is az az epszilon.

Az 1-es számot így ábrázoljuk: 1 = 1*20 , a szám pozitív, így az előjelbit: 0; a karakterisztika (feszítve) 1023+0=102310=1 1111 11112 ; ez 11 biten: 01 1111 1111; az ábrázolandó szám mantisszája 53 biten: 1.00…002, ahol az első bit a rejtett bit, így a mantissza 52 biten: 00…00; azaz a szám: 0 – 01111111111 – 0000000000000000000000000000000000000000000000000000

Ez azt jelenti, hogy az 1-es számban 52 szabad bitünk van a további tizedesjegyek ábrázolására, így a legkisebb azt követő szám a 0 – 01111111111 – 0000000000000000000000000000000000000000000000000001, amely 1 + 2-52 értéket vesz fel, így ez az 1-est követő legközelebbi ábrázolható szám.

Kis gyakorlásnak a 39 ábrázolása: 3910=10 01112; normál alakja: 1.00111x 25, hiszen az utolsó számjegytől 5-tel kell előre tolni a kettedes vesszőt. A karakterisztika (feszítetten) 1023+5 = 102810=100 0000 01002; a mantissza pedig (elhagyva a rejtett bitben tárolt első 1-est): 0011100000…00; azaz a szám: 0 – 10000000100 – 0011100000000000000000000000000000000000000000000000

Látható, hogy itt már csak 52 – 5 = 47 bitünk van a tizedesjegyek számára, így a 39 + 2-47 még ábrázolható, de 39 + 2-48 már nem. A konzolon:

39+2**-46
39.000000000000014
39+2**-47
39.00000000000001
39+2**-48
39

S ahogyan növekszik az 1-nél nagyobb szám karakterisztikája, úgy lesz az egymást követő ábrázolható értékek között lévő rés egyre kisebb. S ez ugyanaz a folyamat, amely oda vezet, hogy a MAX_SAFE_INTEGER-nél nagyobb értékek esetén már 2-esével, majd 4-esével, majd 8-asával … kezdenek ugrani az egymást követő egész számok. (Nyilván ott már nem csak a tizedes jegyeket nem tudjuk ábrázolni, de már az utolsó számjegyek is kezdenek ‘beragadni’ 0-ra.)

Egészen más a helyzet viszont a 0 közelében, azaz a -1 és 1 közé eső számoknál. Fentebb szó volt arról, hogy a denormalizált számok segítségével a legkisebb ábrázolható pozitív szám a MIN_VALUE, a 2-1074. A lebegőpontos ábrázolás az ennél kisebb számokat 0-nak látja.

Az 1-nél kisebb számok ábrázolásához a számítás ui. úgy kezdődik, hogy az adott nulla-egész-valamennyi számot az algoritmus elkezdi 2-esekkel szorozni, amíg el nem jut egy egy-egész-valamennyi számig, s ebből állapítja meg, hogy mekkora lesz az exponens. Erre egy példa a binary-system.base-conversion.ro oldalról:

Itt a 0.01 számot alakítjuk lebegőpontos formátummá. Az algoritmus elkezdi 2-sel szorozni a számot újra és újra. Látható, hogy a 7. szorzásra kapunk olyan számot, amelynek az egészrésze 1, azaz a normált alakban a kitevő -7 lesz. Ezután még 52 lépésben ismétlődik ez a szorzás, hogy megkapjuk a mantissza 52 bitjét. Ha viszont a szorzás az elején eléri az 1073-at és még mindig nulla az egészrész, akkor az ábrázolás 0-nak tekinti a számot, ui. -1073-nál kisebb kitevőt már nem tud ábrázolni a rendelkezésre álló biteken.

Most pedig nézzük meg, hogy mi az 1-nél kisebb számok közül a legnagyobb, azaz az 1-es szám bal oldali tizedestört szomszédja. Ha beírjuk a converterbe, hogy 0.9999, s annyi 9-essel, amennyit csak enged, akkor ezt kapjuk: 0 – 01111111110 – 1111111111111111111111111111111111111111111111111111; itt a kitevő 011111111102 – 102310 = 1022 – 1023 = -1 lesz. Ez a szám úgy néz ki, hogy 1.1111111111111111111111111111111111111111111111111111 x 2-1 = 0.11111111111111111111111111111111111111111111111111111, ahol a nem sárga a rejtett bit. Ez pedig azt jelenti, hogy 53 bitünk van a kettedes pontot követő kettedes jegyek tárolására. Azaz a 2-53 még értelmezhető távolság a következő (alulról) szomszédos számhoz. Nézzük a konzolon:

1-2**-53
0.9999999999999999
1-2**-54
1

Azaz itt 2-53 -asával tudunk lépkedni a 0 felé a szomszédos számokon. Viszont, ha a pozitív (nagy) egész számok felől közelítünk a 0 felé, akkor azt látjuk, hogy a szomszédos kettedes törtek közötti hézagok egyre kisebbek lesznek, s ez a tendencia folytatódik akkor is, ha átlépjük az 1-et és tovább közeledünk a 0 felé. 1-nek a legközelebbi alsó szomszédja 2-53 távolságra van, de egy idő után egyre és egyre közelebb lesznek egymáshoz.

Nézzük pl. az 2-10 = 1/1024 ≈ 0.0009765625 szám környezetét. A konzolon ez így néz ki:

1/1024-2**-62
0.0009765624999999998
1/1024-2**-63
0.0009765624999999999
1/1024-2**-64
0.0009765625
1/1024-2**-65
0.0009765625
1/1024-2**-66
0.0009765625

Azaz a 2-10 szám legközelebbi szomszédja lefelé 2-63 távolságra van tőle, s annál közelebbi szomszédja nincs. Felfelé pedig 2-62 távolságra lesz a szomszédja. Azaz pl. 2-10 egy olyan érték, amelynél lefelé haladva 1 bittel nőni fog az ábrázolás pontossága. Miért?

A 2-10 felső szomszédja a 2-10 + 2-62 ≈ 0.0009765625000000002. Ennek a számnak a lebegőpontos alakja: 0 – 01111110101 – 0000000000000000000000000000000000000000000000000001, azaz az exponens 1013 – 1023 = -10; a szám = 1.0000000000000000000000000000000000000000000000000001 x 2-10 = 0.00000000010000000000000000000000000000000000000000000000000001, ahol a sárga jelzi a mantissza ábrázolásban szereplő bitjeit. Azaz itt az 52 értékes számjegyből már 62 értékes számjegy lesz, mert az eredetileg tárolt 52 bithez hozzájön a 10 számjegynyi eltolás, azaz megnőtt az ábrázolás pontossága. – A pontosság tehát mindig az 52 és az exponens abszolút értékének az összege.

A legkisebb – még normált alakkal – ábrázolható szám nagyságrendje 2-1023, így az ahhoz tartozó szomszéd, mivel 1023 + 52 = 1075, de mivel a legkisebb ábrázolható valós pozitív szám a 2-1074, így a számok közötti rések itt már 2-1074 méretűek lesznek. S ez tartani fog az ábrázolható legkisebb számig, a 2-1074-ig, amelytől a felső szomszédja ugyanekkora – 2-1074 – távolságra van.

Ez a rész a számegyenesen az, ahol a lebegőpontos ábrázolás már a denormalizált alakot használja. Azaz a denormalizált alakkal ábrázolt számok távolsága mindig 2-1074.

A lebegőpontos számok eloszlása a számegyenesen

Erről itt van egy elég jó szemléltetés a courses.engr.illinois.edu oldaláról:

A képen jól látható, hogyan sűrűsödnek be a számok a 0 felé közeledve. A számegyenes a normált lebegőpontos számok eloszlását mutatja. A képen az underflow (alulcsordulás) tartománya az, amit a denormalizált lebegőpontos számok töltenek ki. A denormalizált számok nélkül egy elég nagy lyuk lenne a 0 körül.

Ugyanez a gyires.inf.unideb.hu szavaival: “Egy másik könnyen belátható következménye a lebegőpontos ábrázolásnak, hogy a valós számok tartományának nullához közeli tartományában sokkal több racionális szám ábrázolható pontosan, mint amikor a szám abszolút értéke távolabb van a számegyenesen a nullától. Ezt úgy szokták mondani, hogy a nullához közeli tartományban több az egységnyi szakaszra eső reprezentánsok száma, mint távolabb, vagyis minél nagyobb a szám, annál kisebb az elvárható pontosság.

Még pár szó az EPSILON-ról

A geeksforgeeks.org és még rengeteg oldal ajánlása szerint: “Number.EPSILON property is used to check whether floating-point numbers are equal or not.” Illetve a developer.mozilla.org is azt javasolja a felhasználására, hogy “Testing equality“. Illetve: “Number.EPSILON specifies a reasonable margin of error when comparing floating point numbers. It provides a better way to compare floating point values“.

Fentebb láttunk példát a használatára. Például az a+b=c egyenlőséget lebegőpontos értékekre úgy lehet ellenőrizni, hogy megnézzük, hogy mekkora a+b és c távolsága, s ha ez az érték kisebb mint a Number.EPSILON, akkor az egyenlőség fennáll. Azaz az Math.abs(a+b-c)<Number.EPSILON kifejezés logikai értéke true lesz. Pl. Math.abs(0.1+0.2-0.3<Number.EPSILON)-ra a konzol true értéket ad.

Nézzünk néhány példát arra, amikor ez az eljárás csődöt mond!

Az 1/2-2**-53 értéke a konzolon 0.4999999999999999, s ennek a számnak a lebegőpontos alakja 0 01111111101 1111111111111111111111111111111111111111111111111110, míg az 1/2 lebegőpontos alakja 0 01111111110 0000000000000000000000000000000000000000000000000000, s látható, hogy a két szám eltér egymástól, tehát két különböző lebegőpontos számról van szó, a fenti eljárás viszont egyenlőnek tekintené ezt a két számot, hiszen a távolságuk kisebb, mint az epszilon (2-52).

Math.abs(0.5-0.4999999999999999)<Number.EPSILON
true

Továbbá a stackoverflow.com-on találtam:

This is generally very bad idea. Try to run this one: Math.abs(1.1 + 2.2 - 3.3) < Number.EPSILON The result will be false, because the floating point error here is actually bigger than Number.EPSILON. – Samuel Hapak  Feb 27 ’19 at 15:09 

“it can be used to test for the (approximate) equality of floating-point numbers” Not quite. In a sense, the machine epsilon is like the “significant figure” – the accuracy of a number depends on the mathematical operations you perform and the exponent component in your number. Also see Loss of significance on how inaccuracies can accumulate. – Derek 朕會功夫  Oct 4 ’20 at 22:01

Illetve egy eléggé lesújtó vélemény ugyanott [mere-mortal = akinek nincs meg valamihez a megfelelő szakértelme]:

Q1: What is Number.Epsilon supposed to be used for?

Short answer: it’s just something that uber-nerdy computer scientists might make use of in their calculations.

It is not for mere-mortal programmers. Number.Epsilon is a measure of “approximation error”. To get any practical use out of it, you need to scale it according to the size of the numbers you’re working with.

If you’re not intimately familiar with all the internal workings of floating point numbers, then Number.EPSILON is not for you (frankly I’ve not found a use for it yet in anything I’ve done, so I count myself among the “mere mortals”).

Miért olyan fontos, hogy tisztában legyünk a lebegőpontos ábrázolás korlátaival?

Például azért, hogy ne járjunk úgy, mint az ARIANE 5 hordozórakéta tervezői, amikor is egy 7 milliárd dollár költséggel megépített hordozórakéta 1996-ban a kilövés utáni 40. másodpercben felrobbant, amikor is egy hiba csúszott abba, ahogyan egy 64 bites lebegőpontos számot konvertált a repülést irányító számítógép egy 16 bites egész számmá:

The internal SRI software exception was caused during execution of a data conversion from 64-bit floating point to 16-bit signed integer value. The floating point number which was converted had a value greater than what could be represented by a 16-bit signed integer. This resulted in an Operand Error. The data conversion instructions (in Ada code) were not protected from causing an Operand Error, although other conversions of comparable variables in the same place in the code were protected.” Forrás: www-users.cse.umn.edu

write(), writeln() – egy kis JavaScript történelem

Egy vizsgateszt kitöltése során találkoztam a JavaScript document.write() utasítással – viszont erről az utasításról még az életben nem hallottam, bár már megszoktam, hogy a vizsgatesztben gyakran olyasmi szerepel, amiről egy szó sem esett az (állami) tanfolyamon, de még a leckék után felsorolt dokumentációkban sem. Úgyhogy utána néztem, s elég érdekes dolgok derültek ki.

Egy ma már alig használt metódus

Napjainkban már a böngészőben megjelenő oldalt leképező DOM fa manipulálása a .js fájlban történik, ill. az utasítás tesztelhető a konzolon. Ha ide írom be a document.write(“Hello, world!”) parancsot, akkor a böngészőből eltűnik az eredeti dokumentum, s csak annyi jelenik meg egy üres oldalon, hogy: Hello, word! – Szemben az .innerHTML metódussal, ami documentumon belül változtatja meg egy element – böngészőben megjelenő – tartalmát, s nem tünteti el az eredeti oldalt.

A két eljárás között ez az alapvető különbség, amire rengeteg oldal hívja fel a figyelmet: “document.write() puts the contents directly to the browser where the user can see it“.

Azaz itt a BOM (Browser Object Model) által hivatkozott windows objektumban egy új document objektum jön létre, s az töltődik be a korábbi html fájl helyére – s az eredeti tartalom eltűnik a böngésző ablakából. Az inspektorban is láthatjuk, hogy egy teljesen új DOM fa vette át az előző helyét.

<html>
  <head></head>
  <body>Hello, world!</body>
</html>

A .write() egy document metódus, így csak a documentre tudjuk rátenni – document.write() -, ami azt jelenti, hogy JavaScript fájlból nem tudjuk semmire se használni – mert egyszerűen nincs, amire használhatnánk. Ennek az az oka, hogy ez a metódus még abból az időből maradt fent, amikor még nem lehetett a .js fájlból manipulálni a html oldalt, pl. még nem létezett az .innerHTML metódus sem, ahogyan azt egy JavaScript utasításokat felsoroló oldal is tanúsítja 2001-ből. Ez azt jelenti, hogy anno ezt az utasítást csak a html fájlban használták <script></script> tagok közé inline módon beszúrva.

Ez a metóduscsalád tulajdonképpen négy metódusból áll. A document.open() utasítás indítja el a document stream-et, a document.write() és a document.writeln() jeleníti meg a zárójelek közé stringben írt tartalmat, amelyek html tag-eket is tartalmazhatnak. Végül a document.close() utasítás zárja le a document stream-et.

Ezen belül, ha egy document stream lezárása után hívjuk meg az write() metódust, akkor az automatikusan hívni fogja az open() metódust, ami törölni fogja az addig felépített HTML oldalt és egy új document stream-et indít el. Így az is világos, hogy ha a HTML oldal betöltődése után hívjuk a write() metódust – pl. konzolról, vagy event handler-ként -, akkor az egy új document stream-et indít el, s ezért törlődik az eredeti oldal.

Tehát nagyon fontos: ezt a metódust csak az oldal betöltődése alatt használhatjuk, ha az oldal betöltése után újra használni akarjuk, akkor az törölni fogja és újraírja a tartalmával a teljes oldalt!

Most pedig jöjjön a Holló Színház!

Továbbá a write() és a wrtiteln() – write line – között az a különbség, hogy a writeln() utasítás a megjelenített tartalmat mindig új sorban kezdi, de csak akkor, ha a script <pre></pre> tagok között fut. Egyébként nem. Ez a körülményes használat ma már persze abszurdnak tűnik, viszont a magyarázatához két dolgot kell tudni, ami még ma is érvényben van.

  • A HTML-be írt tartalom ún. white space-ként kezeli a szóközöket és a sortörést, azaz akárhány szóköz vagy sortörés követi is egymást a kódban, azt a program csak egyetlen szóköznek tekinti, csak egy szóközt jelenít meg. Kivéve ezalól az az eset, amikor a tartalom <pre></pre> tagok között van, ekkor ugyanis az összes bevitt szóköz és sortörés is megjelenik.
  • A blackslash (visszaperjel: \) pedig egy ún. escape karakter. Ez azt jelenti, hogy a visszaperjel után leírt karakter olyan utasításként fog kiértékelődni, amelyet egyébként nem tudnánk bevinni a billentyűzetről. Ezek az utasítások még a telexgépek korából származnak, ugyanis kezdetben a számítógépes karakterkódolás a telexgépekre kifejlesztett ASCII kódolást vette át. Ezek közül két vezérlőkód a legfontosabb: a Carriage Return (‘kocsi vissza’ – a kódja ma: \r), ami a nyomtatófej kocsiját vitte a sor elejére a telexgépen, ill. a Line Feed (‘új sor’ – kódja ma: \n), ami a nyomtatófejet a következő sorra állította. A Windows-ban ez a két utasítás egyszerre használatos (\r\n vagy összevonva: \n), s a kurzort az új sor elejére viszi, s onnan kezdi megjeleníteni a további adatokat).
  • Azaz a ‘visszaperjel + n’ karakterek kettősét a számítógép nem megjelenítendő karakternek értelmezi, hanem egy utasításnak.
  • A writeln() pedig ezt a vezérlőkaraktert használja. A HTML dokumentum viszont nem ismeri fel a vezérlőkaraktereket, kivéve azt az egy esetet, amikor az ilyen karaktert tartalmazó content a <pre></pre> tag-ek között szerepel.
  • Ez akkoriban nem okozott különösebb fennakadást, hiszen a script tag-eket inline szúrták a HTML kódba, így nem sok többlet odafigyelés igényelt, hogy elé és utána még beszúrják a pre tag-et is.
  • A writeln(“tartalom”) helyett használhatjuk a write(“<br>tartalom”) utasítást, hiszen a metódus paraméterébe betehetünk HTML tag-et is. A <br> element pedig egy sortörést hoz létre.

Mire volt jó annak idején ez a ma már használhatatlan metódus?

Nézzünk rá egy példát egy 2001-es kiadású JavaScript tankönyvből (Paul McFedries: Special Edition Using JavaScript):

Azaz akkor használatos ez a metódus, ha

  • tartalmat akarunk a HTML betöltődése alatt elhelyezni az oldalon
  • tartalmat akarunk megjeleníteni egy új ablakban úgy, hogy ahhoz ne kelljen külön HTML fájlt létrehozni
  • frame esetén – ezt az esetet nem részletezem, mert az akkoriban használt <frameset> tag HTML5-ben már nem létezik

Itt pedig egy példa egy másik tankönyvből, hogy hogyan használjuk ezt az eljárást arra, hogy a dokumentumban a betöltés folyamata alatt tartalmat jelenítsünk meg:

A kódon látható, hogy a writeln() utasításokat tartalmazó script element pre tagok közé van zárva.
Majd jön a logikus kérdés: mire jó mindez, ha ugyanezt a tartalmat sima HTML kódban is fel tudjuk írni?

A magyarázat az, hogy a write() metódussal nem csak stringeket, de változókat is meg tudunk jeleníteni. Pl. egy document.write(Date()) utasítással meg tudjuk jeleníteni az oldalon az aznapi dátumot, ami viszont egy statikus oldalkezelés esetén lehetetlen lenne.

A másik felhasználási lehetőség, hogy egy új ablakban egy olyan oldalt jelenítsünk meg, amihez nem tartozik HTML fájl. Például:

var newWindow = window.open();
newWindow.document.open();
newWindow.document.write(“Hello, world!”);
newWindow.document.close();

Docker

oktatóvideó: sanfranciscoboljottem.com – Docker ismeretek; Kódbázis: Docker

Telepítés: docker.com/product/docker-desktop; egy program, amit írtunk, adott operációs rendszeren, adott futtatókörnyezetben lett megírva. Ha egy másik gépen akarjuk futtatni, akkor ugyanezt a szoftveres környezetet kell létrehozni azon a gépen. A docker egy elszeparált környezetben (ez a konténer) létrehozza a programot futtató környezetet és elraktározza a programot, majd felteszi ezt egy felhőbe. Ezt a konténert onnan letölthetem más gépre, s ott – a szoftveres környezet nélkül is – futtathatom a programot a konténerből.

A container az image (sablon) lepéldányosított változata. A container a hardverből (processzor, memória, merevlemez) kap egy szeletet és ott fut. Ha beírjuk a parancssorba: docker run hello-world, akkor a következő üzenetet kapjuk (parancssor > docker kliens > docker démon > docker hub)

  1. The Docker client contacted the Docker daemon.
  2. The Docker daemon pulled the “hello-world” image from the Docker Hub.
    (beteszi a gépen lévő lokális cache-be)
  3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
  4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

parancs: docker ps [process status] kiírja az éppen futó konténereket; az utolsó oszlop, a name egy – a docker által adott random – vicces név; docker ps -a [all] a lefutott konténereket is megmutatja; konténer leállítása: docker rm <id> [rm=remove]; az összes konténer leállítása: docker container prune [prune = eltávolít]; a docker run hello-world parancs valójában két parancsot foglal magába: docker create hello-world létrehozza a konténert; a docker start -a <id> lefuttatja a konténert; az -a flag gondoskodik arról, hogy a parancssorba kiíródjon az, ami a konténerből kijön; id-ből elég csak az első pár karaktert beírni, csak annyit, amitől már unikális az id az összes éppen futó közül. Kontéren újrainítása: docker start <id>; a docker run <id> ugyanis a konténer egy újabb példányát hozza létre és futtatja le, míg a docker start <id> ugyanazt a konténert futtatja újra; docker logs <id> paranccsal tudom kiíratni a konténer által eddig kiadott összes outputot; konténer leállítása: docker stop <id>; ez max 10 másodpercig vár, hogy a konténer szabályosan álljon le; a docker kill <id> azonnal leállítja a konténert;

képfájl létrehozása: Dockerfile kiterjesztés nélkül; FROM – base image az alap fájlrendszer létrehozása; WORKDIR: /home/webserver – munkakönyvtár létrehozása a konténeren belül; COPY: ./ ./ – a gyökérkönyvtár fájljait másolja át a konténer gyökérkönyvtárába; ha WORKDIR-rel létrehoztunk egy munkakönyvtárat, akkor a COPY második ./ eleme automatikusan a munkakönyvtárra fog mutatni. RUN – azok a parancsok, amelyek még azelőtt lefutnak, mielőtt a kép elkészült és valaki lepéldányosítaná a konténert; CMD [“parameter”, “parameter”, “parameter”] – azután fut le, hogy valaki elindította a konténert, a parancsban összetartozó kifejezések is külön elemként vannak a tömbben.

A konténer fájlrendszerének kilistázása: docker exec -it <id> sh; exec: execute; -it tkp. két parancs egybeírva: -i -t; i: input – kösse rá a programra ezt a terminált, hogy itt lehessen kommunikálni vele; t: a kommunikáció legyen szépen formázott; a -i -t együtt is írható: -it; sh: shell; ez beléptet a shell-be;itt könyvtár kilistázása: dir; belépés adott könyvtárba cd <könyvtár>

Konténer létrehozása: parancssor: docker build <a Dockerfile elérési útja>; ha abban a mappában vagyok, ahol a Dockerfile is van, akkor a parancs végén egy pont (‘.’) jelzi ezt: docker build . ; a konténer futtatása: docker run <id>; dockerfile kiíratása: cat Dockerfile [cat=concatenate]; névadás a konténernek, hogy ne id-vel kelljen hivatkozni rá; (ehhez regisztrálni kell a docker hub-on): docker build -t felhasználónév/projektnév:verziószám . – a végén a pont!; [t = tagging, taggelés]; verziószám helyére írhatjuk azt is, hogy latest, ekkor a legfrissebbnek fogja tekinteni; indítás: docker run felhasználónév/projektnév:verziószám; a verziószám elhagyható, ekkor a latest-et fogja indítani;

Ha webszervert futtatunk, ami pl. a 3000 portot figyeli, akkor a localhost:3000-en nem jelenik meg semmi a böngészőben, mert az adott port a konténeren belül fut, így azt össze kell kötni a saját számítógépünk 3000-es portjával: docker run -p 3000:3000 felhasználónév/projektnév:verziószám; p: port; -p op. rendszer portja : konténer portja;

Egy Dockerfile egy vs code kódszerkesztővel írt programhoz (a package.json-ba: “scripts”:{ “start”: “node ./src/index.js”}, az index.js-re mutató relatív útnak abból a mappából kell kiindulnia, ahol a package.json van.

FROM node:12
WORKDIR /home/webserver
COPY ./ ./
RUN npm install
CMD ["npm","start"]

Amikor a Dockerfile lefut, akkor soronként jön létre a képfájl egy-egy rétege, amelyek rétegesen épülnek egymásra. Ha változtatunk valamit a Dockerfájlban, akkor a változatlan képeket a cache-ből tölti be, csak a megváltozott rétegeket hozza újra létre. Ez a réteges felépítés arra is jó, hogy ha egy másik képfájlban is szerepel a FROM node:12 utasítás, akkor a docker nem hozza létre újra ezt a réteget, hanem a már meglévőt veszi elő a cache-ből.

Oktatóvideó: A docker konténerizáció

A windowsra letöltött docker program egy linuxos virtuális gépet hoz létre, a ezen az emulált linuxos környezetben futnak a konténerek. A docker info parancs a docker rendszerinformációit mutatja meg. Amikor a konténer fut, akkor container, amiből viszont létrejött, az az image. Az image-k a hub.docker.com-on; docker run <név>: ha nincs meg lokálisan a gépen a kép, akkor felmegy a hub.docker.com felhőjébe és onnan lelölti, majd futtatja; docker pull <név>: csak letölti, ha nincs meg lokálisan; docker images: kilistázza, hogy milyen docker képek vannak a gépemen; konténer futtatása: docker run <név>: a konténer a háttérben fog futni; docker run -it <név>: kapcsolat lesz a futó konténer és az adott parancssor között; pl. ls parancs kilistázza a konténer fájlrendszerét (kis l + s); docker ps: kilistázza a futó konténereket; docker logs <név>: megmutatja a konténer naplófájlját; docker rm: konténer törlése; docker rmi: image törlése [remove]; docker build . :konténer építése image-ből, abban a mappában nyissuk meg a parancssort, amiben a Dockerfile van; docker push <az image neve>; előtte bejelentkezni: docker login;

Ha a program több konténert is futtat, akkor az indító parancs, ami egyszerre kezeli az összes konténert: docker-compose up; ekkor egy docker.compose.yml fájlra van szükség; yml kiterjesztés: egy YAML nyelven írt file; YAML: Yet Another Markup Language (‘egy másik jelölőnyelv’ – ekkoriban szaporodtak el a jelölőnyelvek, pl. HTML); ember által olvasható, adat-szerializálásra való, leginkább konfigurációs fájlt létrehozására szolgáló nyelv.

Példa egy docker-compose.yml fájlra:

version: "3"
services:
  nodejs_tutorial:
    build:
      context: ./server
      dockerfile: Dockerfile
    volumes:
      - "./server/src:/usr/src/app"
    environment:
      - PORT=3000
    ports:
      - "3000:3000"
    networks:
      - tutorial_net
  mysql_host:
    build:
      context: ./db
      dockerfile: Dockerfile
    restart: always
    environment:
      MYSQL_DATABASE: 'test_db'
      MYSQL_USER: 'user'
      MYSQL_PASSWORD: 'password'
      MYSQL_ROOT_PASSWORD: 'password'
    ports:
      - '3308:3306'
    networks:
      - tutorial_net
    expose:
      - '3308'
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    links:
      - mysql_host
    environment:
      PMA_ARBITRARY: 1
      PMA_HOST: mysql
      PMA_PORT: 3306
    restart: always
    ports:
      - 8181:80
    networks:
      - tutorial_net
    volumes:
      - /sessions
networks:
  tutorial_net:
    driver: bridge

ports: – “3000:3000” – a gazdagép portját rákötöm a konténer portjára; ha bekötök a konténerbe a volume-t, akkor a gazdagépből ráerősítek a konténerre egy mappát és engedem, hogy a konténer ebből a fájlból olvasson és ide írjon, azaz ha történik adat átírása / hozzáadása a program futása során, akkor az megőrződik a gazdagépben lévő mappában a konténer törlése után is; enviroment: megadhatok környezeti változókat; networks: network beállítása, amin keresztül az egyes konténerek kommunikálnak egymással;