توابع یا Functions
یک تابع یک یک فرایندی است که ورودیهایی را به عنوان آرگمان تابع دریافت کرده و خروجیهایی به عنوان مقدار بازگشتی تابع بر میگرداند.
توابع میتوانند اهداف زیر را داشته باشند:
۱ـ Mapping یا نگاشت: هدف این توابع تولید خروجی بر پایه مقادیر ورودی یا به عبارتی نگاشت مقادیر ورودی به مقادیر خروجی
۲ـ Procedures یا رویه ها: یک تابع ممکن است فراخوانی شود تا یک دنبالهای از کارهای مختلف را انجام دهد. این دنباله به عنوان یک procedure یا رویه شناخته میشوند و برنامهنویسی به این شیوه را برنامهنویسی procedural یا رویهای میگویند.
۳ـ I/O یا ورودی/خروجی: هدف وجود این توابع ارتباط با بخشهای مختلف سیستم است مانند: صفحهی نمایش، حافظهی سیستم، شبکه و ...
Mapping یا نگاشت کردن
توابع Pure بصورت کامل در رابطه با Mapping است. توابعی که آرگمانهای ورودی را به مقادیر خروجی نگاشت میکنند به عبارتی به ازای هر مجموعه از ورودیها یک خروجی منحصربفرد وجود دارد.
مفهوم referential transparency
در ریاضیات هم توابعی وجود دارد که بسیار مشابه توابع در جاوااسکریپت هستند، مانند تابع جبری زیر:
که به این معناست تابعی با نام f تعریف کردیم که آرگمان به نام x را دریافت میکند و در ۲ ضرب میکند.
برای استفاده از آن به راحتی میتوانیم یک مقدار برای x در نظر بگیریم.
در جبر، عبارت بالا دقیقا برابر نوشتن مقدار 4 است، به عبارتی در هر جایی قابل جایگزین شدن با 4 است بطوریکه مشکلی در محاسبات ایجاد نمیکند. به این خاصیت referential transparency میگویند.
حال بگذارید این مورد را در جاوااسکریپت بررسی کنیم و تابع جبری بالا را به تابع جاوااسکریپت تبدیل کنیم.
const double = x => x * 2;
میتوان خروجی تابع بالا را با استفاده از console.log بررسی نمود:
console.log(double(5)); // 10
همانطور که گفتیم در ریاضیات میتوانستیم را با مقدار 4 جایگزین کنیم در این مورد هم موتور جاوااسکریپت را با 10 جایگزین میکند بنابراین عبارت با عبارت زیر برابر است.
console.log(10); // 10
این خاصیت referential transparency در تابع double به این دلیل است که یک تابع Pure است ولی اگر این تابع pure نبود و شامل side effects یا اثرات جانبی مانند ذخیره سازی اطلاعاتی در حافظه یا لاگ کردن اطلاعاتی در console بود نمیتوانستیم براحتی آن را با یک عدد مثل 10 جایگزین کنیم بطوریکه تغییری در عملکرد برنامه ایجاد نشود.
برای داشتن referential transparency باید از توابع Pure استفاده کرد.
توابع Pure یا Pure Functions
توابع Pure برای اهداف مختلفی ضروری هستن مثل برنامهنویسی فانکشنال، هم زمانی و پیادهسازی کامپوننتهای UX قابل اطمینان.
استفاده از توابع Pure تا زمانیکه امکان پیادهسازی نیازمندیهای یک برنامه با آنها وجود دارد پیشنهاد میشود. توابع Pure ورودیهایی را دریافت کرده و بر پایه آن ورودیها خروجیهایی را بر میگردانند. آنها سادهترین قسمتهای سازندهی کدهای یک برنامهاند که قابل استفادهی مجدد هستند. احتمالا یکی از مهمترین اصول طراحی در علوم کامپیوتر KISS است که مخفف Keep It Simple, Stupid یا Keep It Stupid Simple (به این مسئله اشاره میکند که بیشتر سیستمها اگر ساده نگه داشته شوند بهترین عملکرد خود را خواهند داشت در برابر حالتی که پیچیده شوند.) و استفاده از توابع Pure میتواند بهترین راهی باشد تا نرم افزار ما از اصل Stupid Simple یا همان بصورت احمقانهای ساده پیروی کند.
یک تابع Pure باسد دو شرط زیر را داشته باشد:
۱ـ deterministic باشد یعنی به ازای هر ورودی یکسان داده شده یک خروجی یکسان برگرداند.
۲ـ side effects یا تاثیرات جانبی نداشته باشد.
۱_ deterministic (برای هر ورودی تنها یک خروجی قطعی وجود دارد)
برای مثال به متد double توجه کنید، شما میتوانید همیشه فراخوانی تابع double را با مقدار خروجی آن جایگزین کنید مثلا همیشه برابر مقدار 10 در برنامهی ما هست و فرقی نمیکند در کدام قسمت برنامه، چه زمانی ویا چند بار فراخوانی شود.
اما برای همهی توابع نمیتوان این حرف را زد. بعضی از توابع برای تولید نتایج خروجی از اطلاعاتی خارج از اطلاعاتی که به عنوان آرگمان ورودی برای آنها ارسال میشود استفاده میکنند.
این مثال را در نظر بگیرید:
Math.random(); // 0.4011148700956255
Math.random(); // 0.8533405303023756
Math.random(); // 0.3550692005082965
حتی با توجه به اینکه ما هیچ آرگمان ورودی برای تابع random ارسال نکردیم، هر بار فراخوانی آن خروجیهای متفاوتی تولید کرد و این به این معناست که این تابع Pure نیست.
با هر بار اجرا یک عدد تصادفی بین 0 و 1 تولید میکند بنابراین واضح است که نمیتوان آن را صرفا با مقداری مانند 0.4011148700956255 در برنامه جایگزین کرد بدون اینکه تغییری در رفتار برنامه رخ دهد.
و همینطور بصورت مشابه این اتفاق برای متد time در مثال زیر برای دریافت زمان جاری میافتد.
function time(){
return new Date().toLocaleTimeString();
}
time(); // "5:15:45 PM"
ما نمیتوانیم صرفا این متد را در کل برنامه با مقدار حال حاضر برای مثال جایگزین کنیم زیرا در اینصورت رفتار برنامهی ما تغییر میکند. اگر این جایگزینی را انجام دهیم در یک ساعت بعد که از برنامه انتظار دریافت ساعت جاری را داریم ساعت یک ساعت قبل که جایگزین تابع time شده را دریافت خواهیم کرد.
یک تابع زمانی Pure است که همیشه به ازای ورودی یکسان، خروجی یکسان تولید کند و همینطور امکان دارد تعدادی از ورودیهای مختلف به یک خروجی نگاشت شوند اما امکان ندارد یک ورودی بیش از یک خروجی داشته باشد.
تابع زیر را در نظر بگیرید:
const highpass = (cutoff, value) => value >= cutoff;
همیشه برای ورودی یکسان خروجی یکسان است.
highpass(5, 5); // true
highpass(5, 5); // true
highpass(5, 5); // true
ورودیهای مختلف امکان دارد به خروجی یکسانی نگاشت شوند.
highpass(5, 123); // true
highpass(5, 6); // true
highpass(5, 18); // true
highpass(5, 1); // false
highpass(5, 3); // false
highpass(5, 4); // false
توابع Pure نباید هیچ وابستگی به state های خارجی داشته باشند زیرا در این صورت دیگر deterministic یا referential transparency نخواهند بود.
۲ـ No Side Effects (بدون اثرات جانبی)
یک تابع Pure اثرات جانبی ندارد یعنی هیچ تغییری در state خارجی ایجاد نمیکند. Side Effect به هر تغییر در برنامه خارج از تابع و مقدار بازگشتی آن میباشد که شامل موارد زیر است.
- ایجاد تغییر در هر متغیر یا object عمومی یا global
- لاگ کردن در کنسول با استفاده از console.log
- نوشتن یا نمایش چیزی در صفحهی نمایش
- نوشتن اطلاعات در یک فایل
- ارسال اطلاعات از طریق شبکه
- اجرا کردن هر فرایند خارجی یا عملیات I/O
- فراخوانی هر تابع خارجی دارای Side Effects
در فانکشنال پروگرمینگ غالباً از side effects اجتناب میشود که باعث آسان شدن توسعه یک برنامه، بازنویسی، دیباگ، تست و نگهداری آن میشود. این دلیلی است که اکثر فریمورکها کاربران خود را تشویق میکنند که state و رندر کامپوننتها را بصورت مستقل و جدا از هم در loosely coupled modules یا همان ماژولهایی با چسبندگی کم (ارتباط بین ماژولهای مختلف با هم کم باشد) قرار دهند.
بررسی چند مثال
با توجه به آنچه آموختیم میخواهیم بررسی کنیم که آیا توابع زیر Pure هستند یا خیر.
مثال ۱
let globalState = 0;
function f(x) {
++globalState;
return x;
}
این تابع شرط اول را داراست یعنی deterministic است زیرا به ازای هر x ورودی همان x را برمیگرداند. ولی چون در یک state خارج از محدودهی تابع تغییری ایجاد میکند دارای side effects میباشد پس در نتیجه این تابع Pure نیست.
مثال ۲
function f() {
return Date.now();
}
همانطور که در این مطلب به non deterministic بودن تابع زمان اشاره کردیم در این تابع نیز به دلیل اینکه با هر بار اجرا یک خروجی متفاوت تولید میکند deterministic نمیباشد و در نتیجه این تابع Pure نیست.
مثال ۳
function f(x) {
console.log(x);
return x;
}
این تابع شرط اول را داراست یعنی deterministic است زیرا به ازای هر x ورودی همان x را برمیگرداند. ولی چون در یک state خارج از محدودهی تابع تغییری ایجاد میکند (که در اینجا لاگ کردن در کنسول است) دارای side effects میباشد پس در نتیجه این تابع Pure نیست.
مثال ۴
فرض کنید تابع getUsernameById یک درخواست ساده get ارسال میکند و بدون هیچ تغییری در دیتابیس، اطلاعات کاربر را دریافت کرده و بر میگرداند.
function f(id) {
const username = API.getUsernameById(id);
return username;
}
این تابع به دلیل ارسال درخواست get یا به عبارتی داشتن یک فرایند I/O دارای side effect است و این تابع Pure نیست.
پی نوشت:
وجود side effect در مثال قبلی برای من در زمان نوشتن این مطلب کاملا مشخص نبود به عبارتی یک درخواست Get که تغییری در دیتابیس ایجاد نمیکرد کمی با تعریفهای مختلفی از side effect که بیانگر تغییر در یک state خارجی است متفاوت بود که بعد از جستوجو در منابع مختلف نهایتا از Eric Elliott نویسندهی کتاب “Composing Software” این سوال را پرسیدم که ایشون با قاطعیت گفتن هر فرایند I/O به غیر از آنچه تابع بر میگرداند یک side effect به حساب میآید.