وقتی که در حال نوشتن کد JavaScript کاملا عملکردی هستید، سر و کله زدن با محاسبات stateful میتواند یک عذاب بزرگ باشد. این اتفاق، میتواند به مواردی مانند تعریف متغیرهای غیر عمدی ختم شود.
در این مقاله ۲ قسمتی، هر چیزی که باید درباره state (اصطلاحی که تمام توسعهدهندگان JavaScript در کار خود به آن بر میخورند) بدانید را با تمرکز بر روی واحدهای stateful توضیح خواهم داد.
واحدها چه هستند؟
یک واحد، (Monad) الگویی است که برای توصیف محاسبات به عنوان مجموعهای از قدمها استفاده میشود. واحدها در زبانهای برنامهنویسی تابعی، برای جلوگیری از بروز اثرات جانبی بسیار استفاده میشوند و همچنین میتوانند در دیگر زبانها برای مدیریت پیچیدگی استفاده شوند.
یک واحد، از این کامپوننتها تشکیل میشود:
- Type Constructor - خصوصیتی که یک نوع واحدی برای یک type زیرین میسازد.
- Unit - تابعی که مقدار یک نوع زیرین را در یک واحد wrap میکند.
- Bind - این تابع برای زنجیرهبندی یک عملیات بر روی یک مقدار واحد استفاده میشود.
JavaScript یک نوع داده به نام state به همراه خود دارد. State معمولا به عنوان یک type محصول، با یک type متغیر حاصل تعریف میشود.
شروع کار: راهاندازی
بیایید با ساخت یک شاخه پروژه خالی شروع کنیم.
$ mkdir <name>
$ cd <cd name>
$ touch index.js
این شاخه را در یک ویرایشگر کد باز کنید. من به شخصه از VS Code استفاده میکنم.
همچنین بیایید کتابخانه پرکاربرد crocks را به عنوان یک Dependency بر روی این پروژه نصب کنیم.
$ yarn add crocks
سپس، فایل جدیدی به نام logger.js بسازید و این مثال کد را در آن قرار دهید. به همین صورت، فایل دیگری به نام helpers.js بسازید و این بار این مثال کد را در آن قرار دهید.
فایل logger.js را به عنوان log داخل فایل index.js اضافه کنید. همچنین Constructor (سازنده) State را از کتابخانه Crocks وارد کنید. حال ما آماده برای شروع کار هستیم.
const log = require('./logger')
const State = require('crocks/State')
ساخت یک واحد stateful در JavaScript
با اتمام مراحل راهاندازی خود، میتوانیم یک نمونه State به نام rajat بسازیم. در اینجا، rajat یک type عددی را دریافت میکند و یک رشته را به عنوان حاصل بر میگرداند.
برای ساخت یک State، به یک تابع خاص برای انتقال به Constructor نیاز داریم. این تابع باید یک مقدار state را بپذیرد و یک حاصل را به همراه State برگرداند.
کتابخانه Crocks تابعی به نام Pair را فراهم کرده است که دقیقا همین کار را انجام میدهد. پس بیایید آن را به کد خود وارد کنیم.
const Pair = require('crocks/Pair');
با استفاده از این تابع، میتوانیم به این صورت یک نمونه State بسازیم:
const rajat = State(state => Pair('value', state));
log(rajat);
تا به اینجای کار را ذخیره کنید و دستور node را در ترمینال خود اجرا کنید. پس از آن، خروجی State Function را در ترمینال خواهید دید.
گرچه، از آنجایی که State ما هیچکاری نمیکند، مگر این که به صراحت به آن بگوییم که چه کاری انجام دهد، در واقع هیچ اتفاق خاصی نیفتاده است. به همین علت، باید به State خود مقداری داده بدهیم تا با آن کار کند.
بیانیه log را با متد .runwith، به این صورت مجددا بنویسید:
log(rajat.runwith(1000);
در اینجا، عدد 1000 را به عنوان state اولیه تابع تعیین میکنیم. دستور node را مجددا اجرا کنید. این بار، خروجی Pair(‘value’, 1000) را دریافت خواهید کرد.
اگر میخواهید حاصل را از Pair بیرون بکشید، باید از متد .fst استفاده کنید. البته به طور مشابه، میتوانید از متد .snd برای گرفتن state از Pair استفاده کنید. به عنوان مثال، rajat را مجددا تشکیل دهید تا مقداری عملیات ریاضی انجام دهد، و state آن را با استفاده از متد .snd به این صورت بگیرید:
const rajat = State(state => Pair(state+1000, state));
log(rajat.runwith(1000).snd());
این کار، خروجی 2000 را به ما خواهد داد، و این چیزی است که در state ذخیره خواهد شد. متد .snd را با .fst جایگزین کنید و ببینید که این بار چه خروجیای میگیرید.
State را Map کرده، و ارزیابی کنید
نگاهی به کد بخش قبلی بیندازید. تا به اینجا، هر زمان که میخواهیم حاصل را تغییر دهیم، باید از Constructor استفاده کنیم. اما در عوض، میتوانیم از متد .map برای تزریق یک تابع استفاده کنیم، و بگذاریم خود type به تمام موارد داخلی رسیدگی کند.
برای استفاده از متد map، بیایید یک Constructor جدید بسازیم که state فعلی را برای ما میآورد.
const getState = () => State(state => Pair(state, state));
همانطور که میتوانید ببینید، به سادگی یک نمونه State ساختهایم و آن را هم به عنوان حاصل و هم به عنوان state برگرداندهایم.
حال بیایید از بیانیه log به این صورت استفاده کنیم:
log(getState().runWith(1000).snd());
حال اگر دستور node را در ترمینال اجرا کنیم، خروجی 1000 را به ما میدهد. همچنین جایگزینی متد snd با fst نیز همان خروجی 1000 را خواهد داد.
در فایل helpers.js، تابع جدیدی به نام add بسازید که دو عدد را به عنوان ورودی میگیرد، و حاصل جمع آنها را به عنوان خروجی بر میگرداند.
const add = x => y => x + y;
میتوانیم از این تابع داخل فایل index.js استفاده کنیم. در ابتدا، تابع add را به فایل index.js وارد کنید.
const {add} = require('./helpers.js');
سپس به این صورت از آن در بیانیه log استفاده کنید:
log(getState().map(add(1000).runwith(1000).fst());
دستور node را اجرا کنید، و ببینید که نتیجه 2000 را به عنوان خروجی خواهید گرفت.
نکته شگفتانگیر در اینجا، این است که حاصل بر خلاف state، به یک type محدود نشده است. در واقع، میتوانیم همانطور که میخواهیم، type حاصل را تغییر دهیم. برای درک این اتفاق، تابع جدیدی به نام pluralدر فایل helpers.js بسازید.
const plural = (single, multiple) => num =>
`${num} ${Math.abs(num) === 1 ? single : multiple}`
این تابع دو رشته را به عنوان متن میگیرد، یک عدد را به عنوان تاریخ آن میگیرد، و آن را در قالب رشته بر میگرداند. این تابع را به فایل index.js وارد کنید.
const {add, plural} = require('./helpers.js');
از تابع plural در یک تابع دیگر به نام amazingRajat استفاده کنید.
const amazingRajat = plural('Rajat', 'Rajats');
بیایید از این تابع در بیانیه log به این صورت استفاده کنیم:
log(getState()
.map(amazingRajat)
.runWith(1000)
.fst()
)
خروجی این کد، چیزی به مانند 1000 Rajats خواهد بود. اگر 1000 را با 1 جایگزین کنید، خروجی را به شکل 1 Rajat دریافت خواهید کرد.
نکته: میتوانید با وارد کردن یک جایگزین از کتابخانه crocks، از شر getState خلاص شوید.
const {get} = require('crocks/State');
جایگزینی state با استفاده از توابع
میتوانیم از get برای اجرای تابعی که state را map میکند و حاصل را با نتیجه بروزرسانی میکند، استفاده کنیم.
برای شروع، فایل جدیدی در شاخه این پروژه به نام data.js بسازید و این کد را در آن قرار دهید:
(function(root) {
const burgers =
{ burgers: 4 }
const tacos =
{ tacos: 10 }
root.data = {
burgers,
tacos
}
})(window)
سپس آبجکت burgers را به فایل index.js وارد کنید.
const {burgers} = require('./data');
به این صورت کدنویسی مراحل راهاندازی به اتمام میرسد. حال یک نمونه state جدید به نام getBurgers به این صورت بسازید. همچنین آن را در بیانیه log فراخوانی کنید.
const getBurgers = get()
log(getBurgers.runWith(burgers))
با اجرای دستور node، خروجی Pair({ }, { }) را دریافت خواهید کرد. این چیزی نیست که منتظرش بودیم.
برای رفع این مشکل، باید مقدار burgers را از state بگیریم و به حاصل Pair اضافه کنیم. این کار، میتواند با کمک تابع prop انجام شود.
const prop = require('crocks/Maybe/prop')
تابع prop، مقدار یک ویژگی بر روی یک آبجکت wrap شده در یک Just را به ما میدهد. اگر ویژگی مورد نظر وجود نداشته باشد، خروجی Nothing را خواهیم گرفت. getBurgers را با استفاده از تابع prop به این صورت مجددا بنویسید:
const getBurgers =
get()
.map(prop('burgers'))
این کار، خروجی Just 4 را به ما خواهد داد. اما معمولا، خروجی را بدون هیچگونه wrap شدن میخواهیم. برای این کار، باید از متد evalWith استفاده کنیم؛ زیرا این متد حاصل را unwrap خواهد کرد. از متد evalWith داخل بیانیه log استفاده کنید و خروجی دریافت شده را ببینید.
log(
getBurgers
.evalWith(burgers)
)
اگر یک تابع را به get منتقل کنیم، state را map کرده، و آن را با state جایگزین خواهد کرد، و به این صورت map اضافی را پاک خواهد کرد.
const getBurgers =
get(prop('burgers'))
یک تابع شگفتانگیز دیگر داخل کتابخانه crocks، به نام option وجود دارد. Option، یا بیانیه Just را unwrap میکند، یا نتیجه را بر میگرداند. (که در موارد Nothing، یک خروجی دیگر خواهد بود) در ابتدا، بیایید این تابع را به فایل index.js وارد کنیم.
const option = require('crocks/pointfree/option');
سپس از این تابع داخل getBurgers به این صورت استفاده کنید:
const getBurgers =
get(prop('burgers'))
.map(option(0))
با این کد، خروجی Just 4 را دریافت خواهیم کرد. اگر به هر دلیلی خروجی ما Nothing باشد، مقدار 0 را به عنوان خروجی دریافت خواهید کرد.
State یک واحد stateful را بروزرسانی کنید
محاسبات stateful نیازمند قابلیت این که stateشان به مرور زمان تغییر کند، هستند. اگر میتوانستیم state خود را به مرور زمان به راحتی بروزرسانی کنیم، این برای ما بسیار کاربردی میبود.
داخل فایل index.js، تابع جدیدی به نام putState بسازید. این تابع، یک state داده شده را میگیرد و یک نمونه جدید که state فعلی را نادیده میگیرد را بر میگرداند. این نمونه جدید، یک Pair به همراه Unit به عنوان حاصل خود، و state داده شده به عنوان state خود خواهد داشت. ما باید Unit را از کتابخانه crocks وارد کنیم.
const Unit = require('crocks/Unit');
سپس، بیایید تابع putState را با استفاده از State، Pair و Unit به این صورت بنویسیم:
const putState = state =>
State(() => Pair(Unit(), State))
log (
putState("Taj Mahal")
.runWith("Agra")
)
این کار، خروجی Pair((), “Taj Mahal”) را به ما میدهد. متد runWith را با evalMethod جایگزین کنید و ببینید که واحد () را به عنوان حاصل به دست میآورید.
State متد دیگری به نام execWith فراهم کرده است. استفاده از آن به جای evalWith، state را از Pair، unwrap میکند و حاصل را خروجی میدهد، که در این مورد واحد () خواهد بود.
این متد وقتی که میخواهید state خود را به مقدار اولیه تغییر دهید، بسیار کاربردی است. فایل جدیدی به نام reset بسازید.
const reset = () =>
putState('Taj Mahal')
این تابع مقدار Nothing را میگیرد و نتیجه فراخوانی putState را با مقدار تعیین شده ‘Taj Mahal’ بر میگرداند. فراخوانی putState داخل بیانیه log را با فراخوانی reset جایگزین کنید. در اینجا میبینید که با وجود شروع کردن با Agra، همچنان خروجی Taj Mahal را به عنوان state خود دریافت میکنیم.
نکته: میتوانید با وارد کردن یک جایگزین از کتابخانه crocks، از شر putState خلاص شوید.
const {put} = require('crocks/State');
State یک واحد stateful را تغییر دهید
در بخش قبلی، یاد گرفتیم که چگونه میتوانیم state را بروزرسانی کنیم. گرچه، بروزرسانی state گاهی اوقات میتواند اعمال هر تغییری به stateهایی که بر پایه مقادیر قبلی هستند را برای ما سختتر کند.
بیایید به این که چگونه میتوانیم از توابع برای تغییر دادن مقدار state استفاده کنیم، نگاهی داشته باشیم.
این توابع، نوع خاصی از توابع هستند و باید نوع type مشابه را در ورودی و خروجی خود داشته باشند.
بیایید با ساخت یک کمک کننده Construction جدید شروع کنیم، که یک تابع شامل این که state ما چگونه باید تغییر کند را دریافت میکند.
const modifyState = fn =>
State(s => Pair(Unit(), fn(s)))
در اینجا، تابعی به نام modifyState ساختهایم که یک تابع را میپذیرد، یک نمونه State را بر میگرداند که state فعلی را میگیرد، و یک Pair از Unit را به عنوان حاصل، و نتیجه فراخوانی تابع را به عنوان state جدید بر میگرداند.
قدم بعدی ما، فراخوانی تابع modifyState داخل log خواهد بود. اما قبل از آن، باید تابع دیگری بنویسیم که تغییرات مورد نظر ما را تعریف میکند.
یک const به نام state، شامل یک آبجکت که تعداد مشخصی پیتزا را تعریف میکند، بسازید.
const state =
{pizzas: 0}
ما میخواهیم که تابعمان تعداد پیتزاها را یک عدد افزایش دهد. برای این کار، به تابعی نیاز داریم که یک آبجکت را میگیرد و بر میگرداند.
کتابخانه crocks یک تابع باینری به نام mapProps فراهم کرده است که نیازمندیهای ما برای تغییر state را در خود دارد.
const mapProps = require('crocks/helpers/mapProps');
برای استفاده از mapProps، باید آبجکتی از توابع را اعمال کنیم که مقدار کلیدی داده شده را با نتیجه اعمال مقدار اصلی به تابع فراهم شده، جایگزین میکند. در اینجا، کلید pizzas را هدف قرار میدهید و از تابع add بر روی آن استفاده میکنیم تا مقدار آن را یک عدد افزایش دهیم.
log(modifyState(mapProps({pizzas: add(1)}))
.execWith(state)
)
مطمئن شوید که تابع add را به درستی وارد کردهاید.
const {add} = require('./helpers')
دستور node را اجرا کنید و ببینید که نتیجه {pizzas: 1} را دریافت میکنید. همین!
در اینجا، بخشی از کار ما به پایان میرسد. منتظر بخش دوم باشید.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید