بکارگیری pure function ( توابع خالص ) در جاوا اسکریپت و ترکیب توابع

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 10 خرداد 1397
دسته بندی ها : جاوا اسکریپت

امروز، می خواهم درباره دو مفهوم اساسی در برنامه نویسی توابعی صحبت کنم: pure function ( توابع خالص ) و ترکیب توابع

پس از خواندن، شما قادر خواهید بود:

. pure function ( توابع خالص ) بنویسید، و برتری های اساسی خالص بودن را توضیح دهید.

. در جاوا اسکریپت فعالیت های خالص و ناخالص را از هم جدا کنید.

درک خالص بودن، یک پیش نیاز مهم در پرورش یک ذهنیت تابعی است. حتی می توان گفت که یک پیش نیاز برای درک برنامه نویسی تابعی است.

جدول محتویات:

  1. توابع خالص
  2. مدیریت ناخالصی
  3. خلاصه

توابع خالص:

برای خالص بودن یک تابع، دو چیز باید درست باشند.

شرط اول: یک تابع فقط و فقط زمانی می تواند خالص باشد، که تنها چیز هایی که برای محاسبه خروجی اش استفاده می کند، آرگومان هایی که شما به آن می فرستید، و متغیر های داخلی که داخل خود تابع تعریف شده اند باشند.

همانند همیشه، یک مثال از توضیح واضح تر است.

function greetUser (user, greeting='Hello') {
  return `${greeting}, ${user.firstName} ${user.lastName}!`
}

const joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}

greetUser(joe) // 'Hello, Joe Schmoe!'

دقت کنید که تنها چیزی که تابع greetUser برای محاسبه مقدار بازگشتی اش استفاده می کند، آبجکت user است که به عنوان یک آرگومان به آن می فرستیم.

شرط دوم: یک تابع فقط و فقط زمانی می تواند خالص باشد، که state را خارج از خودش تغییر ندهد.

تابع greetUser بالا، پاسخگوی این شرط است. یک user (کاربر) را دریافت می کند، و یک رشته را باز می گرداند، و  به هیچ وجه به state دست نمیزند.

بیایید مثالی را ببینیم، که در آن این شرط درست نیست :

onst joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}

function impureUpdate () {
  joe.firstName = 'JOE'
}

joe.firstName // 'Joe'

// A
unsafeUpdate() // changes `joe`'s `firstName` property

// B
joe.firstName // 'JOE'

دقت کنید که joe به طور global تعریف شده است، نه در داخل impureUpdate. همچنان، فراخوانی impureUpdate (B) مقدار joe.firstName (C) را تغییر می دهد.

به این ترتیب، میبینیم که فراخوانی impureUpdate، state را خارج از محدوده محیط اجرای تابع تغییر می دهد. این تغییرات پس از بازگردانی مقدار توسط تابع هم ثابت می مانند.

اگر تابعی خارج از محدوده اش تغییر کند، مانند unsafeUpdate، معمولا اثرات جانبی ای به همراه دارد. پس راه دیگر و رایج تر برای بیان این شرط این است: یک تابع فقط زمانی می تواند خالص باشد که اثرات جانبی ای نداشت باشد.

در ضمن، اثرات جانبی شامل مواردی مثل I/O و باز کردن Console است. پس هر چیزی شامل console.log، fs.readFile و ... باشد، از نظر فنی ناخالص است. اما نگران نباشید، به زودی به سراغ این بخش باز خواهیم گشت.

برای خلاصه گیری می توان گفت که تابع فرضی f فقط و فقط وقتی خالص است که این دو شرط بر آن صدق کنند:

  1. f فقط از آرگومان ها و متغیر های داخلی اش برای محاسبه نتیجه اش استفاده می کند.
  2. f اثرات جانبی ای ندارد.

بیایید درباره این که چرا باید اهمیت دهیم صحبت کنیم، و سپس ببینیم که چگونه توابع خالص بنویسیم.

برتری های خالص بودن، یا عدم برتری های ناخالص بودن

سوال منطقی بعدی این است: که چه؟

برای ما، مهم ترین دلایلی که این مسئله به خاطرشان اهمیت دارد، این ها هستند:

  1. توابع خالص قابل پیش بینی بوده، و بدین ترتیب آزمایششان آسان است.
  2. توابع خالص مدیریت را ساده تر می کنند.

بسیاری برتری های دیگر نیز در خالص بودن هستند، اما ما بر روی این ها تمرکز می کنیم.

توابع خالص قابل پیش بینی هستند

دو شرط بالا را به یاد بیاورید.

یک تابع خالص است اگر:

  1. تنها از آرگومان ها و متغیر های داخلی اش برای محاسبه نتیجه استفاده کند.
  2. هیچ اثر جانبی ای نداشته باشد.

بیایید آن ها را دوباره جمله بندی کنیم.

یک تابع خالص است اگر:

  1. از متغیر های غیر داخلی یا خارج از state برای محاسبه نتیجه اش استفاده نمی کند.
  2. در هنگام اجرا state خارجی را تغییر نمی دهد.

این به ما در واضح سازی منظورمان از «قابل پیش بینی» کمک می کند. توابع خالص قابل پیش بینی هستند، به این معنی که همیشه برای یک ورودی، خروجی مشابهی را می دهند.

مثال در درک آن به شما کمک می کند. ابتدا، تابع خالص که در بالا گفتیم:

function greetUser (user, greeting='Hello') {
  return `${greeting}, ${user.firstName} ${user.lastName}!`
}

const joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}

// This is boring...Same. Thing. Every. Time.
greetUser(joe) // 'Hello, Joe Schmoe!'
greetUser(joe) // 'Hello, Joe Schmoe!'
greetUser(joe) // 'Hello, Joe Schmoe!'

اگر joe را به عنوان آرگومانش ارسال کنیم، تابع greetUser برای همیشه ‘Hello, Joe Schmoe!’ را باز می گرداند.

همانطور که می توانید حدث بزنید، تمام توابع به این صورت قابل پیش بینی نیستند.

function greetUserWithTime (user, greeting='Hello') {
  const salutation =`${greeting}, ${user.firstName} ${user.lastName}!`
  const currentTime = `The current time is: ${Date()}.`

  return `${salutation} ${currentTime}`
}

const joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}

// Now, the result is different on every call
greetUserWithTime(joe) // 'Hello, Joe Schmoe! The current time is: Sun Jan 28 2018 16:59:11 GMT-0500 (Eastern Standard Time)'

greetUserWithTime(joe) // 'Hello, Joe Schmoe! The current time is: Sun Jan 28 2018 17:02:11 GMT-0500 (Eastern Standard Time)'

greetUserWithTime(joe) // 'Hello, Joe Schmoe! The current time is: Sun Jan 28 2018 17:04:16 GMT-0500 (Eastern Standard Time)'

دقت کنید که این دفعه، تابع ما هر دفعه که فراخوانی می شود، نتیجه متفاوتی را باز می گرداند، حتی با وجود این که در هر فراخوانی آرگومان مشابه را ارسال می کنیم.

علت این که مقدار متفاوتی دریافت می کنیم، این است که تابع greetUserWithTime به جای این که تنها بر متغیر های داخلی و آرگومان ها متکی شود، بر state خارجی متکی می شود. برای مثال، زمان حال که تابع در آن فراخوانده شده است را نیز نشان می دهد.

از آنجایی که هر بار که greetUserWithTime را فراخوانی می کنیم زمان تغییر می کند، مقدار بازگشتی اش هم تغییر می کند. این باعث می شود که آن تابع ناخالص و غیر قابل پیش بینی شود. برای این که بدانیم چه چیزی را بر می گرداند، باید هر دو آرگومانی که می فرستیم، و زمان فراخوانی را بدانیم.

این یکی از عدم برتری های اصلی توابع ناخالص است. برای درک کاری که می کنند، باید بدانیم کدام بخش های سیستم خروجی اش را تحت تاثیر قرار می دهند.

گرچه، خروجی یک تابع خالص تنها با ورودی اش تعیین می شود. این باعث می شود آن تابع قابل پیش بینی باشد، زیرا خروجی هایش پایدار هستند. به عبارتی دیگر، مقدار برگشتی ای که برای یک ورودی تولید می کند، هرگز تغییر نخواهد کرد. تنها چیزی که برای درک یک تابع خالص نیاز دارید، نحوه دستکاری داده هایی است که به آن می فرستیم.

این که توابع خالص قابل پیش بینی هستند، یعنی آزمایش کردن آنها ساده است.

تابع greetUser را در نظر بگیرید. پیش بینی چیزی که این تابع برای هر آبجکت داده شده توسط کاربر خارج می کند، ساده است. یک آزمایش بسیار ساده به این صورت می باشد:

const joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}

const expected = 'Hello, Joe Schmoe!'
const actual = greetUser(joe)

if (expected === actual) {
  console.log('All good.')
} else {
  console.error('ERROR. Expected: ${expected}...But got: ${actual}.`
}

این را با تابع greetUserWithTime مقایسه کنید. درک این که چرا خروجی آن هر بار متفاوت است، سخت و نا واضح است.

این که توابع خالص state های خارجی را تحت تاثیر قرار نمی دهند و بر آنها تکیه نمی کنند، یعنی آنها مدیریت state را آسان می کنند. نیازی نیست نگران باشیم که فراخوانی تابع خالصمان state کامپوننت های دیگر برنامه مان را تحت تاثیر قرار دهد.

مدیریت ناخالصی:

این ها کارهایی هستند که شما نمی توانید در هنگام نوشتن توابع خالص انجام دهید:

  1. اصلاح state کلی (global). این مورد از مثال های بالا کاملا واضح است.
  2. I/O. خواندن/نوشتن فایل ها یک اثر جانبی است، پس در توابع خالص مجاز نیست. وارد شدن به یک کنسول نیز غیر مجاز است. پس توابع خالص نمی توانند شامل console.log شوند.
  3. AJAX. یک درخواست شبکه به API مشابه نمی تواند به داده هایی که درخواست کردید، یا یک خطا منجر شود.

این حد و حدود در زمینه I/O و AJAX به نظر محدود کننده میاید. اما به یاد داشته باشید، توابع ناخالص اشتباه نیستند. تنها مسئله این است که استفاده بیهوده از آنها درک برنامه را سخت تر می کند.

قانون اصلی این است که داده ها با استفاده از توابع خالص دستکاری شوند، و سپس نتیجه آن دستکاری ها را در یک تابع ناخالص رندر شود.

به زبانی دیگر، به جای این که فقط توابع خالص بنویسیم، (که غیر ممکن است) به سراغ جدا سازی منظم توابع خالص از ناخالص می رویم.

توسعه یک منطق برای جایی که در کد شما ناخالصی بروز می دهد یک قدم حیاتی در توسعه یک تابع است.

جدا سازی توابع ناخالص

بیایید یک مسیر معقول در جهت توسعه توابع خالص را تکرار کنیم:

  1. قبل از کد نویسی، داده هایی که نیاز دارید را شناسایی کنید.
  2. منبع داده های خود را شناسایی کرده، و بارگذاری کنید. این کار ناخالصی را در شکل I/O، AJAX یا نظارت بر تعاملات کاربر شامل می شود.
  3. داده ها را به شکلی که برنامه تان نیاز دارد تغییر شکل دهید. این کار، شاید شامل استخراج و ترکیب تکه های داده ای که نیاز دارید، مانند firstName و lastName و ویژگی های greetUser در بالا باشد. مثلا گرفتن مقادیر آنها، مثل میانگین خرج های یک مشتری در طی ماه اخیر. به طور ایده آل، این کار باید شامل توابع خالص شود.
  4. آن بخش های کدتان که شامل ناخالصی می شوند (I/O، AJAX، رندر کردن به DOM و...) را مشخص کنید و تابعی بنویسید که اثرات جانبی را انباشته می کند. واضح است که این تابع خالص نخواهد بود. داده هایی که نیاز دارد را به یک آرگومان بفرستید، و داده های داخل تابع را دستکاری نکنید.

بیایید این اقدام را در ساخت یک UI ساده که یک GIF تصادفی را بر حسب جستجوی کاربر نشان می دهد، به کار بگیریم.

قبل از شروع، برخی متغیر های سطح بالا که نیاز داریم را تعریف می کنیم.

'use strict';

const image = document.querySelector('#image')
const refresh = document.querySelector('#refresh')
const input = document.querySelector('#search-term')

const state = {
  searchTerm: 'surprise'
}

در اینجا، این موارد را می گیریم:

. تگ image، جایی که GIF را می اندازیم.

. دکمه refresh، که عکس را تعویض می کند.

. عنصر input، جایی که کاربرها query را وارد می کنند.

همچنین یک آبجکت global به نام state ساخته ایم، که متن جستجو را در آن قرار می دهیم.

شناسایی داده ها

برای عملی شدن این کار، به دو تکه داده نیاز داریم:

  1. یک query وارد شده توسط کاربر.
  2. یک URL برای GIF.

از query که توسط کاربر وارد شده برای ارسال یک فراخوانی JAX به GIPHY API استفاده می کنیم. سپس از URL فراهم شده برای تعیین src تگ img در صفحه استفاده می کنیم.

با توجه به این که به state خارجی بستگی دارند، هیچ کدام از توابع خالص نیستند.

بازیابی داده ها

قدم بعدی، پیاده سازی توابع برای بازیابی query وارد شده توسط کاربر، و ارسال درخواست AJAX خواهد بود.

ساده ترین راه برای گرفتن ورودی کاربر، قراردهی یک listener بر روی input است، که وضعیت را در حالت keyup بروز رسانی می کند.

ساده ترین راه برای ارسال درخواست AJAX نیز fetch است. همچنین یک تابع خالص به نام buildUrl برای سر هم کردن کوئری GIPHY می سازیم.

// @impure: handler has side-effects
input.addeventlistener('keyup', function keyuphandler (event) {
  setsearchterm(event.target.value)
})

// @impure: dom manipulation
refresh.addeventlistener('click', function clickhandler (event) {
  updateimage(state.searchterm, image)
})

// @impure: has side-effects (updates global state)
const setsearchterm = searchterm => {
  state.searchterm = searchterm
}

// @pure
const buildurl = searchterm => {
  const url='http://api.giphy.com/v1/gifs/search'
  const apikey = 'fthi8mvnpuogqtuwmugzbdherk0etrqq'

  return `${url}?q=${searchterm}&api_key=${apikey}`
}

// @impure: time-dependen--whether we receive an error response or data depends on network
const fetchurl = url => fetch(url)

دستکاری داده ها

تابع fetchGif یک Promise را بر می گرداند، که با یک response بر طرف می شود.

آن response شامل دسته ای از اطلاعات فراهم شده توسط API می شود. ما تنها به یک تکه نیاز داریم: URL متعلق به GIF.

در این مورد، تنها کاری ای که باید بکنیم گرفتن مقدار این Property، و دور ریختن بقیه است.

// @pure
const pluckImageUrl = gif => gif.images.original.url

تابع pluckImageUrl آن URL که ما برای به روز رسانی src عکسمان نیاز داریم را بر می گرداند.

به روز رسانی DOM

آخرین کاری که باید انجام دهیم، به روز رسانی صفت src در تگ image است.

این کار را در مقررات then در ادامه fetchUrl انجام می دهیم. همچنین یک تابع خالص به نام randomIdex ساخته ایم، که به ما اجازه می دهد یک GIF تصادفی ارائه شده توسط GIPHY API را انتخاب کنیم.

// @pure
const randomIndex = ({length}) => Math.floor(Math.random() * length)

// @impure: Performs AJAX, updates DOM
const updateImage = (searchTerm, image) => {
  fetchUrl(buildUrl(searchTerm))
    .then(response => response.json())
    .then(data => {
      const gifList = data.data
      const index = randomIndex(gifList)

      const gif = gifList[index]
      image.src = gif.images.original.url
    })
    .catch(error => console.error(error))
}

و در اینجا کار به اتمام می رسد.

در اینجا نکته ای وجود دارد. این که جداسازی توابع خالص و ناخالص کمک می کند همه چیز خوانا تر و قابل آزمایش تر شود. این برتری در این مثال به خوبی کد های بزرگ تر قابل مشاهده نیست، اما ظرفیت پیشرفت برای خواناتر بودن باید واضح باشد.

دقت کنید که جداسازی توابع خالص و ناخالص و استفاده از آنها در توابعی مثل updateImage، جدسازی باگ ها را آسان تر می کند. معمولا تازه واردانی را میبینم که با قراردهی یک Click Handler بر روی یک دکمه Refresh، و سپس گرفتن متن جستجوی کاربر به این مشکل بر می خورند. ساخت URL، ارسال درخواست AJAX و به روز رسانی DOM همه در همان Handler هستند.

این معمولا به یک دیباگ سنگین ختم می شود. حتی در موارد ساده، اگر کار نکرد، معلوم نیست که کدام یک از چهار قطعه به مشکل بر خورده اند. نوشتن 4 تابع که هر کدام یک کار را انجام می دهند، تشخیص بخش مشکل دار را آسان تر می کند.

خلاصه:

و در آخر، خالص بودن را درک کردیم. حال بیایید نکات مهم را مرور کنیم.

. اگر یک تابع state خارجی را تغییر دهد، اثرات جانبی دارد.

. یک تابع خالص است، زمانی که: 1) خروجی اش تنها با مقادیر ورودی اش تعیین شود. 2) هیچ اثر جانبی قابل دیدنی نداشته باشد.

. توابع خالص نمی توانند عملیات های I/O، AJAX، DOM یا log کردن را انجام دهند.

. آزمایش کردن توابع خالص معمولا از آنهایی که به state خارجی وابسته اند، آسان تر است.

. توابع ناخالص ذاتا بد نیستند، فقط ممکن است گیج کننده باشند.

. جداسازی توابع خالص و ناخالص تمرین خوبی است.

دلایل بسیار عمیق تری هستند که می گویند خالص بودن قانع کننده است:

. موازی سازی اجرای توابع خالص از آنجایی که state خارجی را اصلاح نمی کنند، ساده است.

. از آنجایی که خروجی یک تابع خالص تماما با ورودی هایش تعیین می شود، به راحتی می توان آنها را حفظ کرد.

. از آنجایی که توابع خالص در اصطلاح خوش برخورد هستند، کمپایلر ها می توانند فرضیاتی درباره نحوه رفتار آنها بسازند، که درباره توابع دارای اثرات جانبی نمی توانند. این به آنها اجازه می دهد بهینه سازی هایی انجام دهند که در هنگام نا خالص بودن تابع غیر ممکن است.

این ها تماما مفهوم های پیشرفته ای در دنیای جاوا اسکریپت هستند، اما همه در جهت نشان دادن این که خالص بودن چقدر می تواند قدرتمند باشد تلاش می کنند.

منبع

مقالات پیشنهادی

تست اپلیکیشن های جاوا اسکریپتی با sinon.js

sinon یک ابزار مستقل برای تست جاسوسی (spy) و mock ها در جاوا اسکریپت هست و با هر فریمورک unit test کار میکنه.

چرا همگان از فرسودگی جاوااسکریپت صحبت می‌کنند ؟

برای جواب این سوال بگذارید در ابتدا بگوییم که منظورمان از فرسودگی جاوااسکریپت چیست. فرسودگی جاوااسکریپت بهترین تعریف برای وضعیت موجود جاوااسکریپت است،...

۱۲ فریمورک قدیمی جاوااسکریپت که امروز هم کاربرد دارند

جاوااسکریپت در میان توسعه‌دهندگان هر روز محبوبیت بیشتری پیدا می‌کند و این موضوع می‌تواند به دلایل مختلفی برگردد. جدای از آنکه جاوااسکریپت به صورت محلی...

چگونه با استفاده از جاوااسکریپت و Pusher یک گراف بلادرنگ ایجاد کنیم

جهان امروز نیاز دارد که همه چیز در آن سریع اتفاق بیافتد. هر روز جریان‌های داده‌ای زیادی توسط سیستم‌های مختلفی ایجاد می‌شود. آن‌ها تصمیم گیرنده بسیاری...