اولین قدمها برای یادگیری برنامهنویسی فانکشنال مهمترین قدمها هستند و در برخی اوقات سختترین قدمها. البته اگر از راه درست وارد شوید چندان هم سخت نخواهد بود.
شفافیت ارجاعی (Referential Transparency)
شفافیت ارجاعی یک اصطلاح برای شرح این موضوع است که یک تابع خالص میتواند با تعریفش جایگزین شود. یک مثال میتواند موضوع را روشن کند.
در ریاضیات وقتی شما فرمول زیر را دارید:
y=x+10
و گفته میشود که:
x=3
شما میتوانید مقدار x را در معادله بالا بگذارید و به نتیجه زیر برسید:
y = 3 + 10
توجه کنید که این معادله هنوز معتبر است. ما میتوانیم همچین جایگذاریی را با توابع خالص نیز انجام دهیم.
در مثال زیر تابعی در زبان Elm داریم که به رشته ورودی علامت نقل قول اضافه میکند:
quote str = "'" ++ str ++ "'"
,و در مثال زیر نحوه استفاده از این تابع را می بینید:
findError key = "Unable to find " ++ (quote key)
در بالا تابع findError وقتی جستجو برای Keyموفقیتآمیز نباشد یک پیغام خطا تولید میکند.
از آنجا که تابع quote خالص است ما میتوانیم در مثال بالا اسم این تابع را با تعریفش جایگزین کنیم:
findError key = "Unable to find " ++ ("'" ++ str ++ "'")
این چیزی است که من آن را بازتولید معکوس Reverse Refactoring مینامم (زیرا این اسم برای من بسیار بامعنیتر است). این عمل میتواند هم توسط برنامهنویسان و هم توسط برنامهها (کامپایلرها یا برنامههای تست) در مورد کد برنامه انجام شود.
این عمل به طور خاص در مورد توابع بازگشتی کمک کننده است.
ترتیب اجرا
خیلی از برنامهها تک ترده ( single-threaded) هستند یعنی در آن واحد فقط یک تکه از کد اجرا میشود. حتی اگر شما یک برنامه مولتی-ترد داشته باشید خیلی وقتها خیلی از تردها به انتظار تکمیل یک عمل I/O مثل فایل یا شبکه بلوکه شدهاند.
در مثال پایین دلیل اینکه چرا ترتیب مراحل مهم است را شرح میدهیم. فرض کنید میخواهیم نان تست درست کنیم و روی آن کره بمالیم، مراحل این کار اینجوری میشود:
۱. نان را در بیاورید.
۲. دو تکه از نان را در توستر قرار دهید.
۳. میزان برشتگی را انتخاب کنید.
۴. اهرم را رو به پایین فشار دهید.
۵. صبر کنید تا تستها بیرون بپرند.
۶. تستها را بردارید.
۷. کره را بیرون بیاورید.
۸. یک چاقوی کره بردارید.
۹. بر روی تستها کره بمالید.
در مثال بالا دو عمل مستقل وجود دارد: برشته کردن نان، و برداشتن کره. آنها فقط در مرحله شماره ۹ به هم وابسته میشوند.
ما میتوانیم مراحل ۷ و ۸ را مستقل از مراحل ۱ تا ۶ انجام دهیم زیرا آنها به هم وابسته نیستند.
اما وقتی که ما این کار را انجام دهیم، کارها پیچیده میشوند:
ترد شماره ۱
--------------
۱. نان را در بیاورید.
۲. دو تکه از نان را در توستر قرار دهید.
۳. میزان برشتگی را انتخاب کنید.
۴. اهرم را رو به پایین فشار دهید.
۵. صبر کنید تا تستها بیرون بپرند.
۶. تستها را بردارید.
ترد شماره ۲
------------------
۱. کره را بیرون بیاورید.
۲. یک چاقوی کره بردارید.
۳. صبر کنید تا ترد شماره ۱ تکمیل شود.
۴. بر روی تستها کره بمالید.
چه اتفاقی برای ترد شماره ۲ میافتد اگر ترد شماره ۱ با مشکل مواجه شود؟مکانیسم هماهنگ کردن این دو ترد چگونه است؟ چه کسی مالک نان تست است، ترد شماره ۱ یا ۲ یا هر دو؟
آسانتر است که برنامهمان را تکترده بنویسیم و درگیر این پیچیدگیها نشویم. ولی بعضی وقتها لازم است که راندمان برنامهمان را به حداکثر ممکن برسانیم، در همچین موقعیتی لازم است طی یک حرکت قهرمانانه تلاش کنیم که یک برنامه مولتی-ترد بنویسیم.
به هر حال ما دو مشکل اساسی با مولتی تردینگ داریم. نخست اینکه نوشتن، خواندن، تست و دیباگ برنامههای مولتی-ترد دشوار است. دوم اینکه بعضی از زبانهای برنامهنویسی مثل جاوا اسکریپت مولتی-تردینگ را پشتیبانی نمیکنند و آنهایی هم که پشتیبانی میکنند پشتیبانیشان چندان جالب نیست.
اما چه میشد اگر ترتیب اجرا مهم نبود و همه چیز به صورت موازی اجرا میشد؟ هر چند که این خواسته دیوانگی به نظر میرسد ولی آنچنان هم دور از دسترس نیست. اجازه دهید یک مثال با زبان Elm برای نشان دادن این موضوع بزنیم:
buildMessage message value = let upperMessage = String.toUpper message quotedValue = "'" ++ value ++ "'" in upperMessage ++ ": " ++ quotedValue
در کد بالا تابع buildMessage مقادیر message و value را میگیرد و یک پیام با حروف بزرگ، یک دونقطه و یک value در میان علامت نقل قول تولید میکند.
دقت کنید که چگونه توابع upperMessage و quotedValue مستقل از هم هستند. ما چگونه این را میدانیم؟
برای استقلال توابع دو چیز باید وجود داشته باشد: نخست آنها باید توابع خالص باشند، این مهم است زیرا آنها نباید تحت تاثیر اجرای همدیگر باشند. اگر آنها خالص نباشند ما هرگز نمیتوانیم بفهمیم که آنها مستقل هستند. در نتیجه ما برای تعیین ترتیب اجرای آنها مجبوریم به ترتیب صدا زده شدن آنها در برنامه تکیه کنیم. این مکانیسمی است که در همهی زبانهای امری استفاده می شود.
دومین چیزی که برای استقلال توابع مهم است این است که خروجی یک تابع به عنوان ورودی تابع دیگر استفاده نشده باشد. اگر این امر در مورد دو تابع اتفاق بیفتد تابع گیرنده مجبور است صبر کند تا تابع فرستنده اجرایش تمام شود.
در مثال بالا توابع upperMessage و quotedValue هم خالص هستند و هم به خروجی همدیگر احتیاجی ندارند درنتیجه میتوانند به هر ترتیب دلخواه اجرا شوند.
کامپایلر میتواند در مورد ترتیب اجرای توابع بدون هیچ کمکی از طرف برنامهنویس تصمیم بگیرد. این کار فقط در زبانهای برنامه نویسی تابعگرا امکان پذیر است زیرا در دیگر زبانها تعیین زمان انشعاب اثرات جانبی (شبکه، فایلها،...) اگر غیر ممکن نباشد بسیار سخت است.
در زبانهای تابعگرا تعیین ترتیب اجرای توابع میتواند توسط کامپایلر انجام شود.
این کار با توجه به اینکه پردازندهها دیگر سریعتر ساخته نمیشوند بلکه به تعداد هستههای آنها افزوده میشود بینهایت مفید است. این به معنای آن است که کد میتواند در سطح سختافزار به صورت موازی اجرا شود.
متاسفانه در زبانهای امری ما فقط در سطوح بالاتر میتوانیم از این هستهها بهره کامل را ببریم و برای این کار مجبوریم در کد برنامه تغییرات زیادی را بدهیم.
در زبانهای تابعگرا ما می توانیم که به صورت خودکار از تعداد بالای هستهها در سطح قابل قبولی بهره ببریم بدون اینکه مجبور باشیم حتی یک خط از کد برنامهمان را تغییر دهیم.
اعلان نوع (Type Annotations)
در زبانهای استاتیک، نوع متغیرها به صورت داخلی اعلام میشوند. یک مثال از زبان جاوا را در زیر زیر می بینید:
public static String quote(String str) {
return "'" + str + "'";
}
توجه کنید که چگونه نوع دادهها با تعریف تابع درهم هستند. این شلوغی وقتی که شما با جنریکها سر و کار دارید حتی بدتر میشود:
private final Map<Integer, String> getPerson(Map<String, String> people, Integer personId) { // ... }
من نوع دادههایی که باید بیرون باشند ولی هنوز با تعریف تابع قاطی شدهاند را به صورت توپُر نشان دادهام (مترجم: متاسفانه باکس کد این سایت جوریه که حالت بولد کلمات رو از بین میبره، پس دنبال کلمات توپُر نگردید!) . شما مجبورید همچین تعریفی را با دقت بخوانید تا نام متغیرها را پیدا کنید.
در زبانهای دینامیک همچین مشکلی وجود ندارد. در جاوا اسکریپت ما میتوانیم همچین کدی را بنویسیم:
var getPerson = function(people, personId) { // ... };
بدون آن همه تعریف نوع مزاحم، این کد برای خواندن بسیار راحتتر است. تنها مشکل این است که ما از امنیت نوع دادههایمان دست کشیدهایم، ما خیلی راحت میتوانیم به این پارامترها چیزهای اشتباه بفرستیم، مثلا برای people یک عدد بفرستیم یا برای personId یک شی بفرستیم.
ما تا زمان اجرای برنامه به این مشکل پی نخواهیم برد که این زمان ممکن است ماهها بعد از تولید کد باشد. در جاوا همچین مشکلی پیش نمیآید زیرا در جاوا همچین کدی اصلا کامپایل نمیشود.
اما چه میشود اگر ما بهترینها را از هر دو دنیای استاتیک و دینامیک داشته باشیم؟ هم سادگی نحوی جاوا اسکریپت، و هم امنیت نوع داده در جاوا؟
معلوم است که ما میتوانیم، در زیر یک تابع با اعلان نوع در زبان Elm را میبینید:
add : Int -> Int -> Int add x y = x + y
توجه کنید که چگونه اطلاعات نوع داده در یک خط جداگانه نوشته شده است. همین جدایی یک دنیا تفاوت ایجاد میکند.
ممکن است شما فکر کنید که در اعلان نوع خطایی وجود درد. من هم وقتی که برای اولین بار این را دیدم همچین فکری کردم. فکر میکردم که باید به جای اولین علامت -> کاما وجود داشته باشد. اما این یک خطا نیست.
وقتی شما آن را با یک پرانتر ببینید کمی برایتان واضحتر میشود:
add : Int -> (Int -> Int)
این اعلان میگوید که add یک تابع است که یک تکپارامتر از نوع Int میگیرد و یک تابع برمیگرداند که آن تابع هم یک تکپارامتر از نوع Int میگیرد و یک Int برمیگرداند.
در زیر یک مثال دیگر از اعلان نوع با پرانتزهای ضمنی میبینید:
doSomething : String -> (Int -> (String -> String)) doSomething prefix value suffix = prefix ++ (toString value) ++ suffix
اعلان بالا میگوید که تابع doSomething یک تکپارامتر از نوع String میگیرد و یک تابع برمیگرداند که این تابع یک تکپارامتر از نوع Int میگیرد و یک تابع برمیگرداند که این تابع یک تکپارامتر از نوع String میگیرید و یک String برمیگرداند.
دقت کنید که چگونه تمام توابع فقط یک تکپارامتر میگیرند، این به این خاطر است که تمام توابع در زبان Elm کاری شده (Curried) هستند.
از آنجا که پرانتزها همیشه به سمت راست اشاره میکنند نوشتنشان ضروری نیست. در نتیجه به سادگی میتوانیم بنویسیم:
doSomething : String -> Int -> String -> String
پرانتزها وقتی ضروری هستند که ما توابع را به عنوان پارامتر ارسال کنیم. در این حالت اگر پراتنزها را ننویسیم اعلان داده مبهم میشود. به عنوان مثال:
takes2Params : Int -> Int -> String takes2Params num1 num2 = -- do something
بسیار متفاوت است از:
takes1Param : (Int -> Int) -> String takes1Param f = -- do something
تابع takes2Params تابعی است که به دو پارامتر نیاز دارد: یک Int و یک Int دیگر. درحالی که takes1Param یک پارامتر میگیرد: یک تابع، که این تابع دو پارامتر از نوع Int میگیرد.
در زیر اعلان نوع را برای تابع map میبینید:
map : (a -> b) -> List a -> List b map f list = // ...
اینجا پراتنزها ضروری هستند زیرا f از نوع (a -> b) است یعنی تابعی که یک تک پارامتر از نوع a میگیرد و چیزی از نوع b برمیگرداند.
در اینجا نوع a میتواند هر چیزی باشد. هنگامی که نوع داده با حروف بزرگ شروع شود نوعش صریح است مثل String. وقتی که نوع داده با حروف کوچک شروع شود نوعش هر چیزی میتواند باشد. a در اینجا هم میتواند String باشد و هم Int.
اگر جایی دید که (a->a) این یعنی اینکه نوع داده ورودی و خروجی باید مشابه باشد، مهم نیست نوعشان چه چیزی باشد مهم این است که حتما مشابه باشند.
اما در تابع map ما داریم که (a->b) این یعنی اینکه نوع داده ورودی و خروجی هم میتواند مشابه باشد هم میتواند متفاوت باشد.
وقتی که نوع داده برای a تعیین شد. a باید در تمام اعلان همان نوع را داشته باشد. به عنوان مثال اگر نوع a را Int و نوع b را String تعیین کنیم اعلان بالا برابر میشود با:
(Int -> String) -> List Int -> List String
در بالا همهی aها با Int جایگزین شدهاند و همهی bها با String.
عبارت List Int به معنی یک List است که حاوی Intها است و عبارت List String به معنای یک List است که حاوی String ها است. اگر در جاوا از جنریکها استفاده کرده باشید این عبارت باید برای شما آشنا باشد.
ادامه دارد...
لینک قسمتهای اول، دوم،سوم و چهارم: +و+و+و+
لینک منبع: +