آموزش زبان برنامه‌نویسی Rust-قسمت۶: کار با تابع + تمرین

حالا که با متغیّرها, دیتاتایپ‌ها, شرط و حلقه ها آشنا شده‌ایم, وقت آن است که سراغ تابع‌ها برویم. تابع(function)ها در سرتاسر کدهایی که به زبان Rust نوشته می‌شوند حضور دارند. همانطوری که در جلسات قبلی دیدیم, نقطه‌ی شروع برنامه‌ها تابع main است. در این جلسه‌ی کوتاه همه‌ی چیزهایی که برای کار با توابع لازم است را یاد می‌گیریم.شما می‌توانید از اینجا نسخه‌ی ویدیویی این آموزش را هم ببینید: https://www.youtube.com/watch?v=PntQsSd-V0g تعریف تابعشکل کلّی یک تابع که مقداری را برنمی‌گرداند(return نمی‌کند) مثل زیر است:fn function_name(arguments) { // Function body }تعریف یک تابع با کلمه‌ی کلیدی fn شروع می‌شود که مخفف کلمه‌ی function است. بعد از fn, نام تابع نوشته می‌شود. در زبان Rust نام توابع‌را به صورت snake case می‌نویسم. یعنی از حروف بزرگ استفاده نمی‌کنیم و کلمات‌را با علامت _ از هم جدا می‌کنیم. بعد از نام تابع, پرانتز قرار می‌گیرد. داخل این پرانتزها آرگومنت‌های ورودی قرار می‌گیرند. بعد از پرانتز, آکولاد می‌گذاریم. کدهای تابع درون آکولادهای باز و بسته قرار خواهند گرفت. بگذارید نگاهی به یک مثال بیاندازیم. تابع ساده‌ی زیر هیچ ورودی و خروجی ای ندارد, تنها یک خط نوشته‌را چاپ می‌کند.fn simple_function() { println!(&quotساده است. نه؟&quot); } fn main() { }اگر برنامه‌ی بالا را اجرا کنیم هیچ خروجی ای دریافت نمی‌کنیم. تنها کامپایلر به ما اخطار می‌دهد که از تابع simple_function استفاده نشده است:warning: function is never used: `simple_function` --> src/main.rs:6:1 | 6 | fn simple_function() { | ^^^^^^^^^^^^^^^^^^^^ | = note: #[warn(dead_code)] on by defaultهمانطوری که جلسات قبلی دیدیم تنها بخش‌هایی که درون تابع main قرار دارند اجرا می‌شوند. به همین دلیل اگر می‌خواهیم تابع ما کار کند, باید آن‌را فراخوانی کنیم. برای فراخوانی یک تابع کافی است اسم آن را بنویسیم و مقابلش پرانتز بگذاریم. پس برنامه‌ی ما این شکلی خواهد شد:fn main() { simple_function(); } fn simple_function() { println!(&quotساده است. نه؟&quot); }حالا خروجی به ما نمایش داده می‌شود:ساده است. نه؟ساده بود, نه؟تابع با پارامترهای ورودیما خیلی وقت‌ها از تابع با پارامترهای ورودی استفاده می‌کنیم تا عملی‌را روی چند داده‌ی ورودی انجام دهیم. برای این کار باید هنگام تعریف تابع, مشخص کنیم که چه ورودی‌هایی دارد و هر پارامتر ورودی از چه نوعی است. پارامترها داخل پرانتزهای جلوی اسم تابع قرار می‌گیرند و مثل متغیّرها تعریف می‌شوند, با این تفاوت که کلمه‌ی let اوّل آن‌ها نمی‌آید. مثلاً فرض کنید می‌خواهیم تابعی بنویسیم که یک عدد را به عنوان ورودی بگیرد و آن‌را پرینت کند. نتیجه می‌شود تابع زیر:fn number_printer(number: i32){ println!(&quotThe number parameter is: {}&quot, number); }خب حالا برای صدا کردن این تابع کافی است اسم آن‌را درون تابع main بنویسیم و درون پرانتزها عددی که می‌خواهیم نمایش داده شود را بنویسیم:fn main() { number_printer(50); } fn number_printer(number: i32){ println!(&quotThe number parameter is: {}&quot, number); }برنامه‌را کامپایل و اجرا می‌کنیم. نتیجه‌اش می‌شود این:The number parameter is: 50فقط به خاطر داشته باشید که نوشتن نوع هر پارامتر الزامی است. قبل از اینکه بتوانیم ادامه بدهیم باید با دو مفهوم بی‌نهایت مهم در زبان Rust آشنا شویم. موقع خواندن بخش بعدی قشنگ حواستان‌را جمع کنید, چون از الان تا آخرین روزی که برنامه می‌نویسید این دو مفهوم با شما همراه خواهند بود.Statement ها و Expression هاما تا همین‌جای کار هم داشتیم از این دوتا استفاده می‌کردیم. Statement ها دستوراتی هستند که اعمالی‌را انجام می‌دهند, بدون اینکه مقداری‌را برگردانند. مثلاً وقتی که یک متغیّررا تعریف می‌کردیم, درواقع داشتیم از یک Statement استفاده می‌کردیم. لازم است که اینجا یادآوری کنم برخلاف زبان‌هایی مثل c یا زبان‌های سطح بالاتری مثل php, تعریف متغیّر در Rust یک Statement است و چیزی‌را برنمی‌گرداند. مثلاً شما نمی‌توانید کدی مثل زیر را بنویسید:fn main() { let a = b = 10; }اگر این کدرا کامپایل کنید با ارور زیر مواجه می‌شوید:error[E0425]: cannot find value `b` in this scope --> src/main.rs:3:13 | 3 | let a = b = 10; | ^ not found in this scopeیا مثلاً اگر برنامه‌ی زیر را کامپایل کنید:fn main() { let a = (let b = 10); }با ارور زیر مواجه می‌شوید:error: expected expression, found statement (`let`) --> src/main.rs:3:14 | 3 | let a = (let b = 10); | ^^^ expected expression | = note: variable declaration using `let` is a statementچرا؟ همانطوری که در متن ارور آخر می‌بینید, بعد از علامت = باید یک expression قرار بگیرد. امّا از آنجایی که تعریف متغیّر یک Statement است, پس کامپایل با ارور روبه‌رو می‌شود. خب احتمالاً الان خودتان تعریف Expression را قبل از اینکه من بگویم می‌دانید. بله, دستوراتی که خروجی دارند. مثلاً اعمال ریاضی همه expression هستند. چون عبارت ۶ + ۱ مقدار ۷ را برمی‌گرداند. همچنین فراخوانی یک تابع یا ماکرو هم یک expression است. شاید برایتان جالب باشد که بدانید یک مقدار عددی تنها هم می‌تواند یک expression باشد. به شرط اینکه بعد از آن ; قرار نگرفته باشد. چون به محض اینکه ; می‌آید, آن دستور تبدیل به یک statement می‌شود.تابع با مقدار خروجیخب مفهوم expression و statement را خوب فهمیدید؟ اینجا با آن‌ها کار داریم. ما خیلی وقت‌ها نیاز داریم که یک خروجی از تابع دریافت کنیم. مثلاً می‌خواهیم تابع ما یک عدد را دریافت کند و مقدار فاکتوریل آن‌را برگرداند. برای اینکه بتوانیم از تابع چیزی‌را خروجی بدهیم, باید هنگام تعریف تابع نوع آن‌را مشخّص کنیم. برای تعیین نوع خروجی یک تابع, باید بعد از پرانتزهای حاوی پارامترها ورودی, علامت <- را قرار دهیم(از چپ به راست: خط فاصله و علامت بزرگتر). حالا پس از این علامت نوع داده‌ی خروجی‌را مشخّص می‌کنیم. مثلاً فرض کنید می‌خواهیم یک تابع بنویسم که ۲ برابر ورودی‌اش را به عنوان خروجی برگرداند. حاصل می‌تواند چیزی شبیه به این باشد:fn duplicator(input_number : i32) -> i32 { let result = input_number * 2; return result; }همانطور که می‌بینید ما دو برابر ورودی‌را در متغیّری به نام result ذخیره کردیم. سپس برای بازگرداندن این مقدار از تابع, نام متغیّررا بعد از کلمه‌ی کلیدی return نوشته ایم. در واقع شما با قرار دادن هرچیزی بعد از کلمه‌ی کلیدی return در تابع, آن را به خارج از تابع می‌فرستید. خب چیزهایی که درمورد expression گفتم را یادتان هست؟ اگر پاسختان مثبت است پس حتماً به یاد دارید که عبارت ریاضی هم خودش یک expression است, البته اگر بعد از آن ; قرار نگیرد. پس تابع بالا را می‌توان این‌طوری هم نوشت:fn duplicator(input_number : i32) -> i32 { input_number * 2 }توابع بازگشتیشما می‌توانید یک تابع‌را درون خودش صدا بزنید. این کار هیچ فرقی با فراخوانی تابع درون تابع main ندارد. مثال زیر را ببینید. در این مثال تابع recursive_function مقدار ورودی‌اش را چاپ می‌کند. سپس اگر ورودی از 1 بزرگتر بود, یک واحد از آن کم می‌کند و همین کار را برای مقدار جدید تکرار می‌کند:fn main() { recursive_function(10); } fn recursive_function(mut input_number: i32) { if input_number < 1 { return; } println!(&quotCurrent input_number is: {}&quot, input_number); input_number -= 1; recursive_function(input_number); }اگر این برنامه‌را اجرا کنیم, خروجی زیر را می‌بینیم:Current input_number is: 10 Current input_number is: 9 Current input_number is: 8 Current input_number is: 7 Current input_number is: 6 Current input_number is: 5 Current input_number is: 4 Current input_number is: 3 Current input_number is: 2 Current input_number is: 1در این برنامه سه نکته‌ی خیلی مهم وجود دارد: 1- چون ما قصد داریم که پارامتر ورودی‌را تغییر بدهیم, قبل از اسم پارامتر کلمه‌ی mut را نوشته ایم. اگر این کار را نمی‌کردیم پارامتر ورودی immutable می‌شد و دیگر نمی‌شد در خط ۱۳ آن‌را تغییر داد. 2- در خط ۱۳ به جای آنکه بنویسیم: input_number = input_number - 1; نوشتیم: input_number -= 1; این کار خلاصه‌تر است, امّا همان معنی‌را دارد. شما می‌توانید تمامی اعمال ریاضی را اینطوری بنویسید: =- , =+ , =/ , =% , =* ۳-با رسیدن به return اجرای تابع متوقّف می‌شود و مقدار مقابل آن برگردانده می‌شود. وقتی که تابع ما چیزی برنمی‌گرداند می‌توانیم از یک return خالی برای متوقف کردن تابع و خروج از آن استفاده کنیم. خب این جلسه هم مثل جلسه‌ی قبلی با هم چندتا مثال حل می‌کنیم تا دستمان با Rust راه بیافتد.تمرینتمرین این جلسه واقعاً ساده است. همانطوری که در متن اشاره شد می‌خواهیم تابع فاکتوریل را به زبان Rust پیاده‌سازی کنیم. ولی چون هدفمان آموزش است, این کار را به شیوه‌های متفاوت انجام می‌دهیم. ۱-تابع فاکتوریل‌را به صورت بازگشتی بنویسید. ۲-تابع فاکتوریل‌را با استفاده از حلقه‌ی for بنویسید. وقتی که روی این تمرین کار کردید, می‌توانید از طریق این لینک پاسخ‌هارا ببینید.من این نوشته‌را خیلی وقت پیش در بلاگ شخصی‌ام منتشر کرده‌ام که می‌توانید با کلیک روی این متن به آن نوشته منتقل شوید و علاوه‌بر دیدن این نوشته در آنجا، بقیه‌ی مطالبی که در ویرگول منتشر نمی‌کنم را هم ببینید.خواندن قسمت قبلیرفتن به ابتدای این مجموعه‌ی آموزشیرفتن به قسمت بعدی