Module ২ · type, operator আর coercion: '5' + 2 কেন 7 না
== কখন, কখন না, আর বাকি judgement call-গুলো
এই lesson-এ যা শিখবে
- Default হিসেবে
===বেছে নিতে পারবে, আরnullআরundefinedএকসাথে ধরতে লিখতে পারবেx == null, একমাত্র==যেটা সবাই মেনে নেয়। Number()আরparseInt,??আর||,toFixedআর সাধারণ number,===আরObject.is: প্রতিটা জোড়া থেকে বেছে নিতে পারবে, কারণসহ।==,===আরObject.is-এর বারো জোড়ার chart পড়তে পারবে, আর বুঝিয়ে বলতে পারবে বেশিরভাগ team কেন===-এর নিয়মটা একটা linter-এর হাতে ছেড়ে দেয়।
Amara Kenji-কে একটা pull request পাঠায়, মানে দোকানের code-এ একটা বদল, review-এর জন্য। if (discount == null) লাইনে Kenji একটাই comment লেখে: "never use =="। Amara-র লাইনটা ঠিক ছিল, আবার Kenji-র নিয়মটাও ভালো। দুটোই সত্যি হতে পারে, কারণ ভালো একটা default মানেই আইন না। এই lesson আগে নিয়মটার পেছনের chart-টা আঁকে, তারপর পাঁচটা judgement call নেয়, প্রতিটার জন্য একটা করে program।
"সমান?" জিজ্ঞেস করার তিন উপায়
JavaScript-এ সমান কিনা দেখার উপায় তিনটা। ===, মানে strict equality, true হয় যখন দুই দিকের type এক আর value-ও এক, কোনো convert ছাড়াই। ==, মানে loose equality, type আলাদা হলে আগে convert করে, lesson 01 আর 02-এ দেখা নিয়মে। Object.is(a, b) এসেছে ES2015-এ, কাজ করে ===-এর মতোই, শুধু দুইটা ব্যতিক্রম বাদে। এই module বারবার যে বারো জোড়ায় ফিরে আসে, তিনটা check সেগুলোর উপর এখানে।
এর চারটা সারি তুমি নিজেই run করে দেখতে পারো। row এক লাইনের একটা arrow function, যেটা এক জোড়ার তিনটা উত্তর print করে।
const out = [];
const row = (label, x, y) => out.push(label + ": " + (x == y) + " " + (x === y) + " " + Object.is(x, y));
row("1 and \"1\"", 1, "1");
row("null and undefined", null, undefined);
row("NaN and NaN", NaN, NaN);
row("0 and -0", 0, -0);
console.log(out.join("\n"));
1 and "1": true false false
null and undefined: true false false
NaN and NaN: false false true
0 and -0: true true false
Chart-টা পড়া
প্রথম দুইটা ফলের কলাম দিয়ে শুরু করো। ছয়টা সারিতে == আর ===-এর মত মেলে না, আর প্রতিটাতেই দুই দিকের type আলাদা, তাই == convert করেছে। Type এক হলে standard বলে দিয়েছে, == হুবহু ===-এর কাজটাই করবে।
এবার সারি 2, 3 আর 4। 0 == "" true, 0 == "0"-ও true, অথচ "" == "0" false। সারি 4-এর দুই দিকই string, তাই কিছুই convert হয় না, আর দুইটা আলাদা text সমান না। মানে == transitive না: a যদি b-র সমান হয় আর b যদি c-র সমান হয়, তাতে a আর c সমান হয়ে যায় না। একে এড়ানোর আসল কারণ এটাই, speed-ও না, style-ও না।
সারি 5, 6 আর 7-এ আছে ব্যতিক্রমটা। ==-এর হিসাবে null আর undefined শুধু একে অন্যের সমান, আর কারও না, 0-এরও না। সারি 8 আর 9 হলো একমাত্র জায়গা, যেখানে Object.is আর === আলাদা পথে যায়। তাই === কখনো চমকে দেয় না, আর == চমকে দেয় ঠিক সেই সারিগুলোতে, যেগুলো তুমি মুখস্থ করোনি।
এই lesson-এর নতুন দুইটা check
value == null
Object.is(a, b)
value == nulltrue হয় যখনvalueহলোnullবাundefined, আর বাকি সব value-র জন্য false।Object.is(a, b)কাজ করেa === b-এর মতো, শুধুNaNসমানNaN-এর, আর0সমান না-0-এর।
x == null, একমাত্র == যেটা লেখার মতো
একটা value দুইভাবে না থাকতে পারে। undefined মানে কেউ সেটা set-ই করেনি, যেমন object-এ কখনো না থাকা একটা property। null মানে কেউ ইচ্ছে করে খালি করে রেখেছে। Setting পড়ার code সাধারণত দুটোকেই "নেই" ধরতে চায়, আর কিছুকে না।
"এটা কি নেই?" জিজ্ঞেস করার তিনটা উপায়, ছয়টা value-র উপর, যাদের সবাইকেই খালি খালি লাগে।
const out = [];
const row = (label, value) => out.push(label + ": == null " + (value == null) + ", === null " + (value === null) + ", !value " + !value);
row("null", null);
row("undefined", undefined);
row("0", 0);
row("empty string", "");
row("false", false);
row("NaN", NaN);
console.log(out.join("\n"));
null: == null true, === null true, !value true
undefined: == null true, === null false, !value true
0: == null false, === null false, !value true
empty string: == null false, === null false, !value true
false: == null false, === null false, !value true
NaN: == null false, === null false, !value true
== null ঠিক দুইটা না-থাকা value-ই ধরে। === null undefined-কে ফসকে যায়। !value ছয়টাই ধরে, তাই 0-এর discount-ও "নেই" হয়ে যেত। Amara-র discount == null-ই ছিল একদম মাপমতো check। লম্বা রূপটা, discount === null || discount === undefined, একই কথা বলে; যে team-এর নিয়মে সব == নিষেধ, তারা এটাই লেখে।
বেশিরভাগ team সিদ্ধান্তটা linter-কে দেয়, আর সেটাই ঠিক
Linter এমন একটা program, যেটা তোমার code পড়ে ঝুঁকির জায়গাগুলো দাগিয়ে দেয়; Module 1-এ নাম এসেছিল ESLint-এর, JavaScript-এর একটা linter। এর eqeqeq নিয়মটা প্রতিটা == আর !=-কে দাগায়। { "null": "ignore" } option দিলে == null ছাড় পায়, আর "smart" option-ও এটাকে ছাড় দেয়। Airbnb-র style guide হুবহু এই setting-টাই দেয়:
module.exports = [
{
rules: {
eqeqeq: ["error", "always", { null: "ignore" }],
},
},
];
এটা একটা ESLint configuration file, eslint.config.js। এটা কিছু print করে না, তাই এর কোনো output নেই। jQuery-র style guide-ও কথায় একই কথা বলে: সবসময় ===, একমাত্র ব্যতিক্রম null দিয়ে null আর undefined একসাথে check করা।
সিদ্ধান্তটা একটা নিয়মের হাতে ছেড়ে দেওয়া ঠিক আছে, review comment-এর চেয়ে বরং ভালো। কাউকে chart মনে রাখতে হয় না, আর review-তে operator নিয়ে না, design নিয়ে তর্ক হয়। Airbnb-র মতো করে সাজানো থাকলে linter Amara-র লাইনটা পাস করিয়ে দিত, Kenji-কে আর comment লিখতে হতো না।
পুরো string-এর জন্য Number, সামনের অংশের জন্য parseInt
Lesson 03-এ দুই রকম পাঠক দেখেছ: Number() চায় পুরো text, parseInt পড়ে সামনের অংশ। কোনটা নেবে, সেটা নির্ভর করে text-টার কী হওয়ার কথা তার উপর। এই module-এর সব converter নিচের flowchart-এ এক পাতায়।
পাঁচটা text, প্রতিটা দুইভাবে পড়া। দোকানের form-এর পরিমাণ (quantity) শুরু থেকে শেষ পর্যন্ত number হতেই হবে; একটা চওড়া বা একটা version নম্বর শুধু number দিয়ে শুরু হয়।
const out = [];
const both = (label, text) => out.push(label + " " + text + ": Number " + Number(text) + ", parseInt " + parseInt(text, 10));
both("quantity", "12");
both("quantity", "2O");
both("quantity", "");
both("width", "250px");
both("version", "22.16.0");
console.log(out.join("\n"));
quantity 12: Number 12, parseInt 12
quantity 2O: Number NaN, parseInt 2
quantity : Number 0, parseInt NaN
width 250px: Number NaN, parseInt 250
version 22.16.0: Number NaN, parseInt 22
"2O"-তে একজন customer শূন্যের জায়গায় ইংরেজি O টাইপ করেছে। parseInt চুপচাপ 2টা বাক্স order করে ফেলে; Number বলে NaN, তখন form আবার জিজ্ঞেস করতে পারে। খালি field দেখায় Number-এর একমাত্র ফাঁদ, 0, যেটা নিয়ে flowchart-এর প্রথম বাক্সটাই সাবধান করে। চওড়া আর version-এ সামনের number-টাই আসল কথা, তাই সেখানে parseInt ঠিক।
0 আর খালি text যখন আসল উত্তর, তখন ??
Lesson 02 দেখিয়েছে, || যেকোনো falsy value পেলেই default-এ চলে যায়, আর ?? যায় শুধু null আর undefined-এ। সিদ্ধান্তটা আসলে data-র ব্যাপার। এখানে 0 কি আসল একটা উত্তর, আর ""?
Bob ওর music player mute করে রেখেছে, তাই save করা volume হলো 0। গানের নামগুলোর মাঝে ও কোনো separator চায়নি, তাই সেটা ""। Theme ও কখনো বেছেই নেয়নি।
const out = [];
const saved = { volume: 0, separator: "", theme: undefined };
out.push("volume with ||: " + (saved.volume || 50));
out.push("volume with ??: " + (saved.volume ?? 50));
out.push("separator with ||: [" + (saved.separator || ", ") + "]");
out.push("separator with ??: [" + (saved.separator ?? ", ") + "]");
out.push("theme with ??: " + (saved.theme ?? "light"));
console.log(out.join("\n"));
volume with ||: 50
volume with ??: 0
separator with ||: [, ]
separator with ??: []
theme with ??: light
|| দিলে Bob-এর mute করা player চালু হয় volume 50-এ, আর separator না চাওয়ার সিদ্ধান্তটা উপেক্ষিত হয়। ?? দুটোই রেখে দেয়, আর যে theme ও কখনো set করেনি, সেটা ঠিকই বসিয়ে দেয়। || ঠিক তখন, যখন খালি মানেই সত্যিই "দেওয়া হয়নি", যেমন একটা খালি display name, যেখানে "Guest" দেখানো উচিত।
toFixed দেখানোর জন্য, হিসাবের জন্য কখনো না
Lesson 03 বলেছিল: আগে হিসাব, শেষে text। এবার এমন একটা bill, যেখানে শুধু ক্রম ঠিক রাখলেও চলে না। 48.50-এর 15 শতাংশ হলো 7.275, আর total 55.775। দুটোই শেষ হয় 5-এ, আর toFixed গোল করে store করা binary value-টাকে, যেটা আসল সংখ্যার চুল পরিমাণ উপরে বা নিচে বসে থাকে।
একই tip দুইবার হিসাব করা: একবার দশমিকে, একবার পুরো cent-এ। Math.round সবচেয়ে কাছের পূর্ণসংখ্যায় গোল করে।
const out = [];
const bill = 48.5;
const percent = 15;
const tip = bill * percent / 100;
out.push("in decimals: tip " + tip.toFixed(2) + ", total " + (bill + tip).toFixed(2));
const billCents = Math.round(bill * 100);
const tipCents = Math.round(billCents * percent / 100);
const totalCents = billCents + tipCents;
out.push("in cents: tip " + (tipCents / 100).toFixed(2) + ", total " + (totalCents / 100).toFixed(2));
console.log(out.join("\n"));
in decimals: tip 7.28, total 55.77
in cents: tip 7.28, total 55.78
প্রথম রসিদ বলছে 48.50 যোগ 7.28 হলো 55.77। Store করা 7.275 আসলে 7.2750000000000003..., তাই উপরের দিকে গোল হয়; আর 55.775 আসলে 55.774999999999998..., তাই নিচের দিকে। Cent-এ 4850 গুণ 15 ভাগ 100 হুবহু 727.5, গোল হয়ে 728, আর পূর্ণসংখ্যা যোগ হয় একদম নিখুঁত। তাই একবার গোল করে পুরো cent-এ নাও, পূর্ণসংখ্যা যোগ করো, আর toFixed call করো শুধু print করার সময়।
NaN আর -0-এর জন্য Object.is
=== দুইটা জায়গায় জেনেশুনেই ভুল বলে, standard এভাবেই বানিয়েছে। NaN === NaN false, lesson 01-এ Zara-র সেই test। 0 === -0 true, অথচ -0 একটা আসল value, যেটা screen-এ এসে হাজির হতে পারে।
একটা আবহাওয়া station একটা reading-এর জায়গায় পাঠায় "n/a", আর -0.4-এর একটা তাপমাত্রা দেখানোর জন্য গোল করা হয়।
const out = [];
const reading = Number("n/a");
out.push("reading === NaN: " + (reading === NaN));
out.push("Number.isNaN(reading): " + Number.isNaN(reading));
out.push("Object.is(reading, NaN): " + Object.is(reading, NaN));
const rounded = Math.round(-0.4);
out.push("rounded: " + rounded);
out.push("rounded === 0: " + (rounded === 0));
out.push("Object.is(rounded, -0): " + Object.is(rounded, -0));
out.push("(-0.4).toFixed(0): " + (-0.4).toFixed(0));
console.log(out.join("\n"));
reading === NaN: false
Number.isNaN(reading): true
Object.is(reading, NaN): true
rounded: 0
rounded === 0: true
Object.is(rounded, -0): true
(-0.4).toFixed(0): -0
=== NaN কখনো কোনো NaN খুঁজে পায় না; রোজকার check হলো Number.isNaN, আর Object.is-ও একমত। Math.round(-0.4) হলো -0: print হয় 0 হিসেবে, === 0-ও পাস করে, কিন্তু toFixed দেখায় "-0", আবহাওয়ার app-এর সেই "-0 ডিগ্রি"। দুই রকম শূন্যকে আলাদা করতে পারে একমাত্র Object.is। এটা তোমার কালেভদ্রে লাগবে, তাতে কোনো সমস্যা নেই।
এটা কোথায় কাজে লাগে
- ESLint।
eqeqeqনিয়মের নিজের page এটা বোঝায়[] == false,[] == ![]আর3 == "03"দিয়ে, তিনটাই true, আর"smart"আরnull: "ignore"option-গুলোর কথা লিখে রাখে। - Airbnb JavaScript Style Guide। ওদের ESLint configuration
eqeqeq-কে set করে"always", সাথেnull: "ignore", তাইx == nullপাস করে আর বাকি সব==আটকে যায়। - TypeScript। Compiler
==নিষেধ করে না। একটাnumber-এর সাথে একটাstring-এর তুলনা দেখলে,==হোক বা===, সে error TS2367 দেয়: "This comparison appears to be unintentional because the types 'number' and 'string' have no overlap." - React। একটা component-এর state set করলে React নতুন value-টাকে পুরনোটার সাথে
Object.isদিয়ে মেলায়, আর দুটো একই হলে আবার render করা বাদ দেয়।
যে ভুলগুলো সবাই করে
১. Kenji-র fix: যে value undefined হতে পারে, তার উপর === null।
const order = { price: 200 };
const discount = order.discount;
const final = discount === null ? order.price : order.price * (1 - discount);
console.log("final: " + final);
final: NaN
কোনো error নেই। Order-এ কখনো discount ছিলই না, তাই সেটা undefined, null না, আর 200 * (1 - undefined) হলো NaN। লেখো discount == null। এই বদলটা তুমি করবে, কারণ "never use ==" শুনলে মনে হয় এর কোনো ব্যতিক্রম নেই।
২. === দিয়ে NaN খোঁজা।
const score = Number("ten");
console.log(score === NaN ? "bad input" : "score " + score);
score NaN
এই check কখনো true হতে পারে না, কারণ ===-এর হিসাবে NaN কারও সমান না, নিজেরও না। লেখো Number.isNaN(score)। তুমি === NaN লিখবে, কারণ পড়তে একদম সোজা ইংরেজির মতো লাগে।
৩. "নেই" বোঝাতে ! ব্যবহার করা।
const quantity = 0;
console.log(!quantity ? "missing" : "quantity " + quantity);
missing
পরিমাণ 0 দেওয়া হয়েছিল, অথচ program বলছে নেই, কারণ 0 falsy। "নেই" বোঝাতে চাইলে লেখো quantity == null। তুমি !-এর দিকে হাত বাড়াবে, কারণ এটা ছোট, আর যে value-গুলো দিয়ে test করেছিলে সবগুলোতেই কাজ করেছে।
Maria chart-এর প্রতিটা সারি নিজের screen-এ দেখতে চায়, lesson যে চারটা চালিয়েছে শুধু সেগুলো না। এই program সারি 1 print করে। Chart-এর ক্রমে সারি 2 থেকে 12 যোগ করো, চালাও, আর প্রতিটা লাইন chart-এর সাথে মিলিয়ে দেখো।
const out = [];
const row = (label, x, y) => out.push(label + ": " + (x == y) + " " + (x === y) + " " + Object.is(x, y));
row("1 and \"1\"", 1, "1");
// add rows 2 to 12 of the chart here, in the chart's order
console.log(out.join("\n"));
আলাদা করে grade হয় না। নিচের কঠিন অনুশীলন three-equalities একই বারোটা সারি grade করে, typed input থেকে।
David-এর form প্রতি লাইনে একটা করে field পাঠায়। না-থাকা field, খালি field আর সত্যিকারের 0, তিনটাকে ওর আলাদা করতে হবে, যেগুলো || এক বস্তায় ভরে ফেলত।
Input. এক লাইনে n, তারপর n-টা লাইন। প্রতিটা লাইন এভাবে পড়ো: হুবহু null মানে null, হুবহু undefined মানে undefined, খালি লাইন মানে "", তা না হলে Number(line), যদি সেটা NaN না হয়, আর তাও না হলে text-টাই।
Output. প্রতিটা value-র জন্য এক লাইন: value == null হলে missing, === "" হলে empty, === 0 হলে zero, আর বাকি সব ক্ষেত্রে value, একটা space, তারপর String(value)।
Constraints. 1 <= n <= 20000। প্রতিটা লাইনে 0 থেকে 20টা অক্ষর, দুই মাথায় কোনো space নেই, আর শেষ লাইনটা কখনো খালি না।
Sample. Input 6, তারপর null, একটা খালি লাইন, 0, undefined, 42 আর abc দিলে ছয় লাইনে missing, empty, zero, missing, value 42 আর value abc।
const input = require("fs").readFileSync(0, "utf8");
const lines = input.split("\n");
let at = 0;
const next = () => lines[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
missing-empty-zero নামে grade হয়। এটা starter-এর lines variant, তাই next() পুরো একটা লাইন ফেরত দেয়, খালি লাইনও। দুই রকম না-থাকা value-র জন্য একটাই == null check ব্যবহার করো।
যে chart Zara নিজে run করেনি, তাতে ওর ভরসা নেই। ও প্রতিটা জোড়া typed token হিসেবে লেখে, আর প্রতিটার তিনটা উত্তর চায়।
Input. এক লাইনে n, তারপর n-টা লাইনে x y। প্রতিটা token-এর একটা type আছে: num:-এর পরে text মানে Number(text), তাই num:NaN, num:-0 আর num:Infinity-ও চলবে। str:-এর পরে text মানে ওই string, আর শুধু str: মানে ""। বাকি token-গুলো bool:true, bool:false, null আর undefined।
Output. প্রতিটা জোড়ার জন্য এক লাইনে তিনটা শব্দ: x == y, x === y আর Object.is(x, y), যেমন true false false।
Constraints. 1 <= n <= 20000। প্রতিটা token-এ বড়জোর 25টা অক্ষর, ভিতরে কোনো space নেই, আর num:-এর পরের text কখনো খালি না।
Sample. Input 4, তারপর চার লাইনে num:1 str:1, null undefined, num:NaN num:NaN আর num:0 str: দিলে true false false, true false false, false false true আর true false false।
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
three-equalities নামে grade হয়। t.slice(4) একটা token-এর প্রথম চারটা character বাদ দিয়ে বাকিটা দেয়, তাই "num:-0".slice(4) হলো "-0"; এটা শেখাবে Module 3। Hidden test-এ chart-এর বারোটা জোড়াই আছে, আরও আছে।
সচরাচর যে প্রশ্নগুলো আসে
== nullযদি ঠিক থাকে, তাহলে যেখানে==ঠিক উত্তর দেয়, সেখানেই ব্যবহার করি না কেন?কারণ তখন প্রতিবার chart-টা মনে রাখতে হবে, তোমাকেও, পরের পাঠককেও। সারি 4 দেখিয়েছে
==transitive-ও না।== nullএকমাত্র case যেটা মনে রাখার মতো ছোট: দুইটা value, আর কিছু না।===কি==-এর চেয়ে fast?Kenji এটা জিজ্ঞেস করে, আর প্রশ্নটাই ভুল। দুই দিকের type এক হলে standard
==-কে হুবহু===হিসেবেই সংজ্ঞায়িত করেছে। এই lesson কিছুই মাপেনি, তাই বেছে নাও মানে দেখে, speed দেখে না।===-এর বদলে সব জায়গায়Object.isলিখব?না। এটা আলাদা শুধু
NaNআর দুই রকম শূন্যের বেলায়, আর পাঠক আশা করে===দেখবে।NaN-এর জন্যNumber.isNaNব্যবহার করো, আরObject.isরাখো সেই বিরল code-এর জন্য, যাকে0আর-0আলাদা করতে হয়।TypeScript কি আমাকে
==লিখতে দেয় না?দেয়। দুইটা type-এর মধ্যে কোনো মিল থাকা সম্ভব না হলে সে TS2367 দেয়, তুমি যে operator-ই লেখো। কোন operator চলবে, সেটা ঠিক করা linter-এর কাজ,
eqeqeqদিয়ে।
মূল কথা
- Default হলো
===: এক type, এক value, কোনো convert নেই, কোনো table মনে রাখার দরকার নেই। x == nullএকমাত্র==যেটা সবাই মেনে নেয়: এটাnullআরundefinedধরে, আর কিছু না, আর linter একে ছাড় দিতে পারে।- যে value-কে শুরু থেকে শেষ পুরোটা number হতে হবে, তার জন্য
Number(); সামনে থাকা number-এর জন্যparseInt(text, 10)বাparseFloat। - 0 আর
""যখন আসল উত্তর, তখন??; যখন প্রতিটা falsy value মানেই "দেওয়া হয়নি", তখন||। - টাকা রাখো পুরো cent-এ, আর
toFixedcall করো শুধু print করার সময়;Object.isহলোNaNআর-0-এর জন্য। - আরও গভীরে: Under the Hood, abstract equality algorithm আর floating point (Pro)।
এরপর lesson 05 engine-এর ভিতরটা খোলে, আর chart-এর প্রতিটা == সারির পেছনের algorithm-টা এক ধাপ এক ধাপ করে হেঁটে দেখায়।
lesson ৪ শেষ
শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।
পরেরটা: ভেতরের কথা: abstract equality algorithm আর floating point