این مقاله درباره یادگیری اصول برنامهنویسی تابعی یا کتابخانههای برنامهنویسی تابعی JavaScript نیست. تعداد زیادی مقاله خوب درباره این موضوع وجود دارند. این مقاله درباره ماجراجوییها و عواقب جا به جایی به JavaScript تابعی در یک پروژه است.
وقتی که این داستان آغاز شد، من یک برنامهنویس حرفهای با بیش از ده سال تجربه بودم. اول C++، بعد C# و سپس Python. من میتوانستم هر چیزی را برنامهنویسی کنم. اعتماد به نفس من در الگوها و اصولی که به دست آورده بودم، تا جایی گسترش یافت که دیگر یادگیری یک چیز جدید را منطقی نمیدیدم. با خود فکر میکردم: «من ۹۰ درصد برنامهنویسی را بلدم.»
خوشبختانه در می ۲۰۱۶، ما توسعه دهی پروژه XOD را شروع کردیم. XOD یک IDE برنامهنویسی بصری برای سرگرمیهای الکترونیکی میباشد. برای این که مشتریهای بیشتری جذب کنیم، ما مجبور بودیم که یک نسخه وب از این IDE را نیز داشته باشیم. وب؟ یا همان JavaScript! یعنی یک IDE کاملا شکفته شده در JavaScript؟ بله، ما تنها با jQuery خلاصه و مختصر به جایی نخواهیم رسید؛ ما به چیز بهتری نیاز داریم.
در آن زمان، یک فناوری جدید برای توسعه دهی سنگین frontend در حال ظهور بود: چیزی به نام React و الگوهای Flux / Redux که آن را همراهی میکردند. این دو در اسناد و مقالهها، به شدت با مفاهیم برنامهنویسی تابعی در هم آمیخته شده بودند. من شروع به بررسی برنامهنویسی تابعی کردم.
این کار به مانند این بود که یک قاره جدید را کشف کرده باشم. به نوعی استرالیای توسعه دهی، که برنامهنویسان در آن به بالا و پایین میروند و جریانهای داده هم در سمت دیگر راه همین کار را انجام میدهند. البته که درباره Haskell، OCaml و LISP شنیده بودم، اما فکر میکردم که توسعه دهندگان این چنینی، به نوعی روشنفکران حاشیهای هستند که فقط برای این که برنامهنویسی کرده باشند، برنامه مینویسند؛ نه این که بخواند محصولاتی را تولید کنند. باور من درباره خبرگی خودم سریعا به اتمام رسید.
XOD محصولی است که اصول برنامهنویسی تابعی و واکنشپذیر را در ژن خود دارد. تا قبل از این که توسعه دهی آن شروع شود، این مسئله خیلی واضح نبود. بسیاری از چیزهایی که من اختراع کردهام یا از محصولات دیگر قرض گرفتهام، در واقع پایههای برنامهنویسی تابعی هستند. پس همه چیز با هم تطابق داشت، و ما تصمیم گرفتیم که یک محیط برنامهنویسی تابعی واکنشپذیر دارای مقداری JavaScript تابعی واکنشپذیر مدرن بسازیم.
طبق پیش بینیهای ما، ارزش تلاش مورد نیاز را داشت. برنامهنویسی تابعی یک چارچوب خیلی محکم و منعطف به این پروژه داد. من دیگر نمیخواهم به برنامهنویسی کلاسیک نگاه کنم و قطعا تمام پروژههای جدید را در آینده با اصول برنامهنویسی تابعی توسعه خواهم داد.
شکستن مانع
شما میتوانید تعداد زیادی کتابخانه برنامهنویسی تابعی بر روی NPM بیابید. یکی از قابل توجهترین موارد، Ramda است. این کتابخانه نوعی lodash یا Underscore است، اما با این تفاوت که در درجه اول برنامهنویسی تابعی را مد نظر دارد. Ramda چندین تابع به شما میدهد تا دادههای خود را پردازش کنید و توابع را بسازید.
توابع به تنهایی خوب هستند، اما شما به برخی آبجکتهای برنامهنویسی تابعی برای کار کردن با آنها نیاز خواهید داشت. یک کتابخانه دیگر به نام Ramda Fantasy آنها را به شما خواهد داد. شما همچنین ممکن است متوجه برخی کتابخانههای برنامهنویسی تابعی در حال رشد دیگر مانند Sanctuary، Fluture و Daggy بشوید. پس از این که کمی راه افتادید آنها را هم بررسی کنید، اما برای این که گیج نشوید فقط با Ramda شروع کنید.
اولین مانعی که به آن بر میخورید در اینجاست. اگر به اسناد هر کتابخانه برنامهنویسی تابعیای نگاه کنید، در بهترین حالت با چندین سوال «یعنی چه؟» مواجه خواهید شد. ترتیب آرگومان، واژه شناسی خارجی و مقدار کاربردی ناواضح برخی توابع شما را تحریک خواهند کرد که دست از تلاش بردارید و به سراغ برنامهنویسی عادی باز گردید. پس...
نکته اول: یادگیری برنامهنویسی تابعی را با مقالههایی شروع کنید که مختص یک زبان یا کتابخانه خاص نیستند. در ابتدا نیاز خواهید داشت که مفاهیم پایه را بررسی کنید، منفعتهای آن را درک کنید و نحوه تغییر شکل کد فعلی خود را ارزیابی کنید.
بسیاری از مقالههای درباره برنامهنویسی تابعی، توسط ریاضیدانان درس خوان نوشته شدهاند. خواندن آنها بدون آموزشهای مقدماتی خطرناک است؛ زیرا دستهبندیها و مورفیزمها میتوانند بدون هیچ گونه بازدهی ذهن شما را گیج کنند.
دیوانگی بی هدف
یکی از اولین مفاهیم غیر طبیعی که در هنگام بررسی برنامهنویسی تابعی یاد میگیرید، برنامهنویسی ضمنی است که همچنین با نام استایل بی هدف یا کدنویسی بی هدف شناخته میشود.
ایده اصلی آن، نادیده گرفتن نامهای آرگومان تابع یا به طور دقیقتر، نادیده گرفتن آرگومانها به صورت کلی است:
export const snapNodeSizeToSlots = R.compose(
nodeSizeInSlotsToPixels,
pointToSize,
nodePositionInPixelsToSlots,
offsetPoint({ x: WIDTH * 0.75, y: HEIGHT * 1.1 }),
sizeToPoint
);
این یک تعریف تابع معمولی است که به کلی با ترکیب توابع دیگر ساخته شده است. با این که یک فراخوانی به آرگومانهای ورودی نیاز خواهد داشت، باز هم این تابع هیچ آرگومان ورودیای ندارد. حتی بدون یک پیشزمینه، میتوانید درک کنید که این تابع به عنوان یک حامل عمل میکند، که یک اندازه را میگیرد و مقداری مختصات پیکسل را تولید میکند. برای یادگیری جزئیات دقیق، شما به تابعی که شامل این ترکیب میباشد وارد میشوید. این توابع هم ممکن است ترکیبی از توابع دیگر باشند و این روند همینطور پیش میرود.
این یک تکنیک بسیار قدرتمند است، تا این که به نقطه پوچی برسید. وقتی که ما شروع به استفاده از حقههای برنامهنویسی تابعی کردیم، مشکل تبدیل هر چیزی به یک چیز بی هدف را به عنوان یک پازل در نظر گرفتیم، که باید همینطور آن را حل کنیم و حل کنیم:
// Instead of
const format = (actual, expected) => {
const variants = expected.join(‘, ‘);
return `Value ${actual} is not expected here. Possible variants are: ${variants}`;
}
// you write
const format = R.converge(
R.unapply(R.join(‘ ‘)),
[
R.always(“Value”),
R.nthArg(0),
R.always(“is not expected here. Possible variants are:”),
R.compose(R.join(‘, ‘), R.nthArg(1))
]
);
شما آن را حل کردید! حال این پازل را با دیگرانی که میخواهند کد را بازبینی کنند به اشتراک بگذارید.
سپس شما واحدها یا مونادها (monadها) و خلوص (purity) را یاد می گیرید. خب، حال دیگر توابع من نمیتوانند اثرات جانبی داشته باشند. آنها نمیتوانند به this ارجاع کنند، آنها نمیتوانند به زمان و مقادیر تصادفی ارجاع کنند، و آنها نمیتوانند به هر چیزی به جز آرگومانهایی که به آنها داده شده است، ارجاع کنند؛ حتی به constantهای رشته global و عدد پی در ریاضی. شما آرگومانهای ضروری، factoryها و مولدهایی از توابع خارج از محدوده را از طریق زنجیره تو در تویی به توابع داخل محدوده حمل میکنید، شما علائم را منفجر میکنید، و سپس موناد Reader یا State را یاد میگیرید. شما نقشهها و زنجیرههای پراکنده کل کد خود وارد میکنید، و محصول نهایی شما آماده است.
پس، با ترکیب کنندهها آشنا شدیم! چه هیولاهای جالبی. در ضمن ترکیب کننده Y (Y-combinator) فقط یک شتاب دهنده نیست، بلکه یک جایگزین بازگشتی (recursion) هم هست. بیایید بار بعدی که به یک مشکل قابل حل با استفاده از بازگشت (recursion) یا یک فراخوانی «reduce» ساده بر میخوریم، از آن استفاده کنیم.
نکته دوم: برنامهنویسی تابعی فقط درباره حسابداری lambda، مونادها، مورفیزم و ترکیب کنندهها نیست. بلکه درباره داشتن توابع قابل تولید کوچک و به خوبی تعریف شده، بدون جهشهای global state، آرگومانهایش و IO است.
به زبانی دیگر، اگر استایل بی هدف آن به شما کمک میکند تا در مواردی خاص بهتر با آن ارتباط برقرار کنید، از آن استفاده کنید. در غیر این صورت، از آن استفاده نکنید. فقط به این دلیل که میتوانید، از مونادها استفاده نکنید. وقتی از آنها استفاده کنید که یک مشکل را حل میکنند. در ضمن، آیا میدانستید که Array و Promise هم موناد هستند؟ اگر نه، همچنان این مسئله شما را از اعمال صحیح آنها باز نمیدارد. باید بینش خود را به قدری گسترده آموزش دهید که بتوانید درک کنید یک موناد مورد نیاز است، یا نه. این کار کمی تمرین میبرد. تا زمانی که دلیل مناسبی ندارید، از چیزهای جدید بیش از حد استفاده نکنید.
یا یک exception را نمایش بده، یا null (خالی) را برگردان
یکی از ابعاد جا به جایی به استایل برنامهنویسی تابعی خیلی مرا آزار میداد. در JavaScript کلاسیک، شما حداقل دو گزینه برای نمایش یک خطا دارید:
- برگرداندن null (خالی) / undefined به جای یک نتیجه
- نمایش یک exception
وقتی که برنامهنویسی تابعی را انتخاب میکنید، شما همچنان این گزینهها را دارید و به علاوه، مونادهای Either و Maybe را نیز دریافت خواهید کرد. حال چگونه باید خطاها را مدیریت کنیم؟ API عمومی کتابخانه من چه ظاهری باید داشته باشد؟
از یک نظر Maybe / Either یک راه «مناسبتر» است، اما این مونادها ممکن است برای کاربران کتابخانهها ناآشنا باشند. Nullها و exceptionها مرسوم هستند، اما همیشه به پیغام «undefined is not a function» در کنسول ختم میشوند. خلاصه داستان این که...
نکته سوم: از مدیریت خطا از طریق Maybeها و Eitherها نترسید. این زوج بهترین گزینههای شما در دنیای مونادها هستند.
وضوح، یک دارو است
وقتی که در یک پروژه توسعه داده شده با اصول برنامهنویسی تابعی همکاری میکنید، به سرعت متوجه عواقب آن میشوید. حال انجام دادن یک بررسی مجدد، نیازمند بار شناختی کمتری میباشد. اگر به تابع نگاه کنید، کد آن باید تنها چیزی باید که دربارهاش فکر میکنید. دیگر مجبور نیستید که عواقب جهش دادن این فیلد در قبال آن کامپوننت را تصور کنید. دیگر فکر نمیکنید که یک کپی سطحی در اینجا مناسبتر است، یا یک کپی عمیق. دیگر نیازی نیست به چیزی بیش از ۱۰ خط کدی که به آن نگاه میکنید فکر کنید.
حال وقتی که به یک کد قدیمی نگاه میکنید، این کد همیشه مشکوک به نظر میرسد. چرا این کد یک فیلد را در آبجکت من تغییر میدهد؟ چرا آن را در فیلد مورد نظر ذخیره میکند؟ آیا آبجکت من را بدون اجازه جهش خواهد داد؟ کد کلاسیک فقط خیلی اشتباه به نظر خواهد رسید.
نکته چهارم: شما مجبورید کتابخانه ها و همکاران سازگار با برنامهنویسی تابعی را انتخاب کنید. مورد دوم (همکاران) به خصوص خیلی مهم است. یک بخش تیم برای برنامهنویسی تابعی تلاش میکند، بخش دیگر آزادانه اصول را اجرا میکند، و در نهایت برنامهنویسی تابعی در پروژه شکست خواهد خورد.
استخدام کردن توسعه دهندگان JavaScript تابعی سختتر است؛ زیرا این کار یک سطح حداقلی بالا را تعیین میکند. اما وقتی که یک مورد را پیدا میکنید، احتمال این که بهترین فرد را برای پروژه خود یافته باشید بالاتر است. در پروژه XOD ما همگی با برنامهنویسی تابعی سازگاریم، و من خوشحالم که ما با هم کار میکنیم.
منفعتها قربانیهایی را به همراه دارند
برنامهنویسی تابعی به قدری نسبت به mainstream متفاوت است، که ابزاری که mainstream را در هدف دارند دیگر کار نخواهند کرد.
Flow و TypeScript در کار کردن شکست میخورند، زیرا برای آنها سخت است که آن همه چند ریختگی آرگومان را بیان کنند. با این که پیوستگیهایی برای Ramda وجود دارند، پیغام نهایی بسیار رمزنگاری شده و ناواضح است. برای مثال وقتی که آنها اغلب به شما هشدار اشتباهی میدهند، و وقتی که یک خطا به صورت قطعی وجود دارد.
شما میتوانید برخی کتابخانهها را بیابید که در حین رانش بررسیهای تایپ را اجرا میکنند. افسوس که آنها به خوبی مقیاس نمیپذیرند. تاوان کارایی آنها اغلب بسیار بالاتر از هزینه اجرای تابع در ثانیه است.
اگر در یک ترکیب عمیق دچار اشتباه شوید، برای مثال کمی انواع ورودی و خروجی را به هم بریزید، در هنگام دیدن trace کار اشکتان در خواهد آمد.
Error: Can’t find prototype Patch of Node with Id “HJbQvOPL-” from Patch “@/main”
at /home/nailxx/devel/xod/packages/xod-func-tools/dist/monads.js:88:9
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:860:20
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:7:53
at src/project.js:887:5
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:860:20
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45
at _filter (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_filter.js:7:9)
at /home/nailxx/devel/xod/node_modules/ramda/src/filter.js:47:7
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_dispatchable.js:39:15
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry2.js:20:46
at f1 (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry1.js:17:17)
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45
at src/typeDeduction.js:171:37
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:864:20
at src/project.js:618:33
at _Right.chain (/home/nailxx/devel/xod/node_modules/ramda-fantasy/src/Either.js:67:10)
at src/project.js:617:8
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:860:20
at _map (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_map.js:6:19)
at map (/home/nailxx/devel/xod/node_modules/ramda/src/map.js:57:14)
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_dispatchable.js:39:15
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry2.js:20:46
at f1 (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry1.js:17:17)
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45
at validateProject (src/project.js:1031:3)
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27
at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45
at src/flatten.js:1021:5
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23
at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:864:20
at Context.<anonymous> (test/flatten.spec.js:1805:27)
وقتی که به یافتن منبع مشکل میرسیدم اکثر این trace بی معناست. خوشبختانه، وقتی که کد برنامهنویسی تابعی به موفقیت برای بار اول اجرا شود، شما میتوانید مطمئن باشید که بسیار محکم است و در آینده باعث تعجب نخواهد بود. اگر در حال انجام برنامهنویسی تابعی در JavaScript هستید، یکی از عواقب واضح آن نیاز به یک واحد آزمایش کامل است.
کد برنامهنویسی تابعی بیشتر شبیه به CSS است تا JavaScript. آیا عاقلانه است که یک نقطه شکست به CSS قرار دهیم و آن را قدم به قدم اجرا کنیم؟ پوشش فایل CSS چیست؟ در جاهایی که شما از استایل اخباری به امری جا به جا میشوید، این ابزار کار خواهند کرد؛ اما حال کد شما برای devtools قطعه قطعه شده است و تجربه شما در کدنویسی به شدت خواهد کرد.
نکته پنجم: وقتی که برنامهنویسی تابعی را تجربه کنید، ناراحت و عصبی خواهید بود. من وقتی که از ویندوز به لینوکس مهاجرت کردم، همین حس را تجربه کرده بودم و درک کردم که هر دوی آنها مضخرف هستند و هیچ راهی برای از بین بردن این فکر ندارم. همین مسئله در جا به جایی از یک IDE به Vim هم وجود داشت. امیدوارم منظورم را درک کنید.
آیا ما میتوانیم بهترین هر دو را به دست بیاوریم؟ از برنامهنویسی تابعی استفاده کنیم، اما در عین حال دیوانه نشویم و تجربه توسعه دهی خوبی داشته باشیم؟ به نظر من بله.
مقالات مرتبط:
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید