یکی از سختترین وظایف در برنامه نویسی کنترل پیچیدگی برنامه است. بیتوجهی به این مسئله منجر به رشد بیش از حد پیچیدگی و اندازه برنامه میشود. در این مطلب، به یکی از مفاهیم بسیار مهم برنامه نویسی که برنامه نویسی تابعی است میپردازیم. این مفهوم میتواند برای تحت کنترل نگه داشتن پیچیدگی و نوشتن برنامههای بهتر، به شما کمک کند.
در انتهای این مطلب با برنامه نویسی تابعی، انواع توابع و قوانین برنامهنویسی تابعی آشنا شده و به درک بهتری از توابع Higher Order دست خواهید یافت.
برنامهنویسی تابعی یک الگو یا سبک برنامهنویسی است که متکی بر استفاده از توابع Pure و منفرد است.
همانطور که احتمالا حدس میزنید، استفاده از توابع، جزء اصلی برنامه نویسی تابعی است اما صرفا استفاده از توابع به معنی برنامه نویسی تابعی نیست.
در برنامهنویسی تابعی از توابع Pure استفاده میکنیم که در ادامه به طور مفصل توضیح خواهیم داد.
ابتدا با برخی اصطلاحات و انواع توابع آشنا میشویم.
[button class=”github-btn” href=”http://frontcast.ir/procedural-oop-functional”]ویدیوی آموزشی: برنامه نویسی رویهای، شیگرا و تابعی[/button]
در جاوااسکریپت تمام تابعها، از نوع توابع First Class هستند. به این معنی که میتوان با این نوع توابع مانند متغیرهای دیگر رفتار کرد.
تابعهای First Class، توابعی هستند که به عنوان مقدار متغیرها، به عنوان مقدار return در دیگر توابع یا به عنوان آرگومان در توابع میتوان از آنها استفاده کرد.
در مثال زیر به یک ثابت، یک تابع به عنوان مقدار داده ایم:
const helloWorld = () => { console.log("Hello, World") // Hello, World }; helloWorld()
توابع Callback، توابعی هستند که به عنوان آرگومان به دیگر توابع داده شده و توسط آنها فراخوانی میشوند.
به بیان سادهتر، توابع Callback توابعی هستند که به عنوان آرگومان در توابع دیگر مینویسیم. ما نمیتوانیم توابع Callback را فراخوانی کنیم بلکه این توابع زمانی فراخوانی میشوند که تابع اصلی که به عنوان آرگومان این توابع را گرفته است فراخوانی شود.
const testValue = (value, test) => { if (test(value)) { return `${value} passed the test` } else return `${value} did not pass the test` }; const checkString = testValue('Name', string => typeof string === 'string') checkString
testValue تابعی است که یک value و یک تابع Callback با نام test را میگیرد. تابع Callback دومین آرگومانی است که به تابع testValue میدهیم. به طوری که اگر مقدار value هنگام فراخوانی تابع Callback، برابر true باشد، عبارت “value passed the test” برمیگرداند.
توابع Higher Order، توابعی اند که توابع دیگری را به عنوان آرگومان میگیرند یا یک تابع را برمیگردانند. در ادامه بیشتر با این نوع توابع آشنا خواهیم شد.
توابع Asynchronous، توابع بدون نام هستند که نمیتوان آنها را مجدد استفاده کرد. این توابع معمولاً زمانی نوشته می شوند که ما نیاز داشته باشیم کاری را یک بار و فقط در یک مکان انجام دهیم.
const checkString = testValue('Name', value => typeof value === 'string') checkString
chackString متغیری است که مقدار آن یک تابع است که دو آرگومان به آن میدهیم.
Name اولین آرگومان و دومین آرگومان یک تابع Asynchronous است که بدون نام بوده و تنها وظیفه آن چک کردن مقدار داده شده برای value است که از نوع رشته باشد.
[button class=”github-btn” href=”http://frontcast.ir/async-javascript”]ویدیوی آموزشی: فرآیندهای Asynchronous در جاوااسکریپت[/button]
در اوایل مقاله به این نکته اشاره کردیم که صرفا استفاده از توابع به معنی برنامه نویسی تابعی نیست. اگر بخواهیم برنامههای ما واجد شرایط استاندارد برنامه نویسی تابعی باشند، برخی اصول وجود دارد که باید آنها را یاد بگیریم. در ادامه به بررسی این اصول میپردازیم.
اولین اصل برنامه نویسی تابعی اجتناب از تغییرات بی مورد است. یک تابع نباید چیزهایی مانند یک متغیر سراسری را تغییر دهد. این یکی از اصلهای بسیار مهم در برنامه نویسی تابعی است. زیرا چنین تغییراتی منجر به بوجود آمدن باگهایی در کد میشود.
برای مثال اگر یک تابع یک متغیر سراسری را تغییر دهد، ممکن است منجر به رفتار غیرمنتظره در تمام مکانهایی شود که از آن متغیر استفاده میشود.
دومین اصل برنامهنویسی این است که تابع باید Pure باشد. به این معنی که نباید Side Effect داشته باشد. در برنامهنویسی تابعی تغییراتی که ایجاد میشوند Mutation و خروجیها Side Effect نامیده میشوند. در یک تابع Pure هیچ یک از این دو اتفاق نمیافتند. یک تابع Pure همیشه برای ورودیهای یکسان، خروجی یکسان تولید میکند.
اگر تابعی به یک متغیر سراسری وابسته باشد، آن متغیر باید به عنوان آرگومان به تابع داده شود تا بتوانیم خروجی یکسان برای ورودی یکسان تولید کنیم.
const legalAge = 21 const checkLegalStatus = (age, legalAge) => { return age >= legalAge ? 'Of legal age.' : 'Not of legal age.' }; const johnStatus = checkLegalStatus(18, legalAge) johnStatus legalAge
[button class=”github-btn” href=”http://frontcast.ir/javascript-function-execution”]ویدیوی آموزشی: اجرای توابع در جاوااسکریپت[/button]
با استفاده از تجرید میتوان جزئیات را مخفی نگه داشت و بتوان مسئله را در یک سطح بالاتر بدون نیاز به داشتن جزئیات پیادهسازی آن بررسی کرد.
برای مثال به جای گفتن “من میخواهم پول را با وسیلهای مبادله کنم که با اتصال به برق تصاویر متحرک همراه با صدا نمایش میدهد” به احتمال زیاد فقط بگوییم “من میخواهم یک تلویزیون بخرم”.
در این مورد، خریدن و تلوزیون اصطلاحات انتزاعی هستند که صحبت کردن را آسانتر میکنند و احتمال گفتن جملات اشتباه را کاهش میدهند. با این همه قبل از استفاده از اصطلاحات انتزاعی مانند خریدن، باید معنی آنها را کامل درک کنیم.
توابع امکان استفاده از چنین مفهومی را برای ما فراهم میکنند. ما برای تسکهایی که بارها و بارها به احتمال زیاد تکرار خواهیمکرد، توابع را میسازیم. به عبارتی توابع به ما امکان میدهند تا انتزاعات مورد نظر خود را پیاده سازی کنیم.
علاوه بر پیاده سازی انتزاعات، برخی از توابع از قبل برای انتزاع وظایفی ایجاد شدهاند که احتمال استفاده از آن ها خیلی زیاد است.
در ادامه برخی از این توابع که جزء توابع Higher Order هستند و برای انتزاع تسکهای تکراری ایجاد شدهاند را بررسی میکنیم.
زمانی که با ساختمان دادههایی مانند آرایهها کار میکنیم، به احتمال زیاد با موقعیت هایی رو به رو میشویم که تنها به آیتمهای مشخصی از آرایه نیاز داریم.
برای بدست آوردن آیتمهای مورد نظر در یک آرایه میتوانیم تابعی مانند مثال زیر توسعه دهیم.
function filterArray(array, test) { const filteredArray = []; for (let item of array) { if (test(item)) { filteredArray.push(item) } } return filteredArray }; const mixedArray = [1, true, null, "Hello", undefined, "World", false] const onlyStrings = filterArray(mixedArray, item => typeof item === 'string') onlyStrings
filterArray تابعی است که به عنوان آرگومان یک آرایه و یک تابع Callback میگیرد. در این تابع از یک حلقه برای بررسی هر عضو آرایه استفاده میشود که در هر تکرار آن آیتم را با استفاده از تابع test بررسی میکند و در صورتی که جزء آیتم های مورد نظر ما باشد به آرایه جدیدی با نام filteredArray اضافه میکند.
با استفاده از این تابع میتوانیم آرایه را فیلتر کرده و آیتمهایی از آن را که مورد نظرمان هستند جدا کنیم.
فرض کنید که ده برنامه مختلف داشته باشیم و در هر یک از این برنامهها بخواهیم یک آرایه را فیلتر کنیم. نوشتن یک تابع تکراری برای هر برنامه به شدت خسته کننده است.
خوشبختانه آرایه متد استانداردی به نام filter دارد. این متد یک آرایه جدید برمیگرداند که عناصر آن از بین عناصر آرایهای است که به عنوان ورودی میگیرد.
const mixedArray = [1, true, null, "Hello", undefined, "World", false] const stringArray = mixedArray.filter(item => typeof item === 'string') stringArray
با استفاده از متد استاندارد filter همان نتایجی را بدست میآوریم که با استفاده از تابعی که در مثال قبلی خودمان تعریف کردیم بدست آمد. لذا متد filter انتزاعی است از تابعی که نوشتیم.
[button class=”github-btn” href=”http://frontcast.ir/course/data-structures-and-algorithms”]دوره ساختمان داده و الگوریتمها در جاوااسکریپت[/button]
فرض کنید یک آرایه از آیتمها داریم که میخواهیم عملیات مشخصی روی تمام آیتمهای آن انجام دهیم. برای این کار میتوانیم تابعی مانند کد زیر بنویسیم.
function transformArray(array, test) { const transformedArray = [] for (let item of array) { transformedArray.push(test(item)) } return transformedArray }; const ages = [12, 15, 21, 19, 32] const doubleAges = transformArray(ages, age => age * 2) doubleAges
به همین سادگی تابعی ایجاد کردیم که با استفاده از یک حلقه تمام آیتمهای آرایه را براساس تابع Callback ای که مشخص میکنیم، تغییر میدهد.
اما باز هم اگر مجبور باشیم تابع را برای بیست برنامهی متفاوت بنویسیم کاری زمانبر و خستهکننده است.
در این مورد نیز قبلا متد استانداردی برای آرایهها با نام map توسعه داده شد که دقیقا همین عملیات را برای ما انجام میدهد. به عبارتی تابع Callback را روی تمام آیتمهای آرایهی داده شده اعمال کرده و نتیجه را که یک آرایه جدید است، برمی گرداند.
const ages = [12, 15, 21, 19, 32] const doubleAges = ages.map(age => age * 2) doubleAges
در برخی موارد آرایهای از اعداد داریم که میخواهیم مجموع این اعداد محاسبه شود. برای انجام چنین کاری میتوانیم تابعی مانند این کد بنویسیم.
function reduceArray(array, test, start) { let sum = start; for (let item of array) { sum = test(sum, item) } return sum } let numbers = [5, 10, 20] let doubleNumbers = reduceArray(numbers, (a, b) => a + b, 0) doubleNumbers
مشابه مثالهای قبلی که دیدیم، آرایهها متد استانداردی با نام reduce دارند که منطق مشابهی با تابعی که نوشتیم، دارد.
متد reduce در مواردی استفاده میشود که میخواهیم یک آرایه را بر اساس تابع Callback خود به یک مقدار واحد کاهش دهیم. این متد آرگومان اختیاری دومی میگیرد که با استفاده از آن میتوان مشخص کرد عملیات را در تابع Callback از کجای آرایه میخواهیم شروع شود.
تابع Callback استفاده شده در متد reduce دو پارامتر دارد. اولین پارامتر به طور پیش فرض اولین آیتم آرایه است. دومین پارامتر آیتم جاری در آرایه است.
let numbers = [5, 10, 20] let doubleNumbers = numbers.reduce((a, b) => a + b, 10) doubleNumbers
تمام آرایهها متد some را دارند که یک تابع Callback میگیرد. در صورتی که برای حداقل یک عنصر از آرایه که تابع Callback برای آن فراخوانی شده شرط تابع صدق کند مقدار true را برگردانده. در غیر این صورت مقدار false برمیگرداند.
const numbers = [12, 34, 75, 23, 16, 63] console.log(numbers.some(item => item < 100))
این متد دقیقا عملکردی متضاد با متد some دارد. به طوری که یک تابع Callback گرفته و در صورتی که تمام آیتمهای آرایه شرط تابع Callback را پاس کنند، مقدار true برگردانده و در غیر این صورت مقدار false برمیگرداند.
const numbers = [12, 34, 75, 23, 16, 63] console.log(numbers.every(item => item < 100))
متد concat متد استانداردی برای آرایهها است که دو آرایه را به هم الحاق میکند و یک آرایه جدید برمیگرداند.
const array1 = ['one', 'two', 'three'] const array2 = ['four', 'five', 'six'] const array3 = array1.concat(array2) array3
متد دیگری که برای آرایه ها وجود دارد متد slice است که آیتمهای یک آرایه را با شروع از ایندکس مشخصی در یک آرایه جدید کپی میکند. این متد دو آرگومان میگیرد.
اولین آرگومان ایندکسی است که کپی کردن آیتمها را از آن شروع میکنیم. دومین آرگومان ایندکسی است که کپی آیتمها را تا آن انجام میدهیم.
در نهایت این متد یک آرایه جدید به ما برمیگرداند که آیتمهای آرایه اصلی از ایندکس آغازین تا ایندکس پایانی در آن کپی شده است.
const numbers = [1,2,3,4,5,7,8] console.log(theArray.slice(1, 4))