۳ - ۳چاپ نوشته‌های ساده

ببینیم چطور میشه نوشته‌ها رو در REPL چاپ کنیم:

Prelude> print "hello world!"
"hello world!"

اینجا با ‏‎print‎‏ دستور چاپ رو به GHCi دادیم، اون هم چاپ کرد (با نقل قول ها دو طرفش). تابع ‏‎print‎‏ مختصِ نوشته‌ها نیست، و برای چاپ تنوعی از داده‌ها استفاده میشه.

مثال زیر هم به GHCi دستور چاپ میده، ولی فقط برای تایپِ ‏‎String‎‏ کار می‌کنه:

Prelude> putStrLn "hello world!"
hello world!
Prelude>

Prelude> putStr "hello world!"
hello world!Prelude>

احتمالاً متوجهِ فرق بین ‏‎putStr‎‏ و ‏‎putStrLn‎‏ شدین. اتفاق دیگه‌ای هم که افتاد، چاپ نوشته‌ها بدون علائم نقل قول بود. دلیل این اتفاق تفاوت باطنی بین ‏‎print‎‏ و این دو تابع‌ه؛ شاید ظاهراً شبیه باشن، ولی تایپ‌هاشون فرق داره. توابعی که ظاهر مشابه دارن، بسته به تایپ یا رَده ‌ای که بهش تعلق دارن، ممکنه رفتار متفاوتی داشته باشن.

ببینیم چطور میشه همین کارها رو توی فایل انجام بدیم. کدهای زیر رو تو یه فایل به اسم ‏‎print1.hs‎‏ بنویسین:

-- print1.hs
module Print1 where

main :: IO ()
main = putStrLn "hello world!"

وقتی این فایل رو تو GHCi لود کنید و ‏‎main‎‏ رو اجرا کنین، چنین چیزی می‌بینید:

Prelude> :l print1.hs
[1 of 1] Compling Print1
Ok, modules loaded: Print1.
*Print1> main
hello world!
*Print1>

احتمالاً مثل بالا، prompt ِ شما هم به اسمِ ماژول تغییر کرده. اگه بخواین می‌تونین با دستورِ ‏‎:module‎‏ یا ‏‎:m‎‏ ماژول رو تخلیه کنین تا prompt دوباره ‏‎Prelude‎‏ بشه. کار دیگه‌ای هم که میشه کرد، تغییر prompt به یه نوشته‌ی دلخواهه، اینطوری با بارگذاری و تخلیه ِ ماژول‌ها، تغییر نمی‌کنه*:

Prelude> :set prompt "new prompt!! "
new prompt!! :set prompt "\x03bb> "
λ> :r 
Ok, modules loaded: Print1.
λ> main
hello world!
λ>
*

می‌تونین تغییرش رو با نوشتن اون دستور داخلِ فایلِ ‏‎~/.ghci‎‏ دائمی کنین. اگه این فایل وجود نداره، باید درستش کنید.

برگردیم به کد، وقتی یه برنامه رو می‌سازین یا تو REPL اجرا می‌کنین، ‏‎main‎‏ دستورالعملِ پیش‌فرض برای اون برنامه‌ست. ‏‎main‎‏ در واقع تابع نیست، بلکه معمولاً یک سِری از دستورات برای اجراست، که ممکنه شاملِ اعمال توابع و ایجادِ عوارض هم باشند. موقعِ ساخت ِ پروژه با Stack، داشتنِ یه فایل به اسم ‏‎Main.hs‎‏ که توش یه دستورالعمل ِ ‏‎main‎‏ باشه، الزامی‌ِه. ولی همونطور که قبلاً هم دیدیم، ممکنه فایلی ‏‎main‎‏ نداشته باشه و بدون مشکل تو GHCi بارگذاری‌ش کنیم.

همونطور که می‌بینین، ‏‎main‎‏ دارای تایپِ ‏‎IO ()‎‏ است. آی-او، یا I/O، مخففِ input/output یا ورودی/خروجی ِه. ‏‎IO‎‏ یه تایپِ خاص در هسکل‌ه، و برای اجرای برنامه‌هایی که علاوه بر محاسبه‌ی توابع یا بیانیه‌ها، شامل اثرات هم میشن به کار میره. چاپ روی صفحه یک اثر ِه، پس چاپِ خروجیِ یک ماژول هم باید داخل این تایپِ ‏‎IO‎‏ پوشونده بشه. موقع‌هایی که یه تابع رو مستقیماً در REPL وارد می‌کنین، GHCi به طور ضمنی تشخیص میده و ‏‎IO‎‏ رو پیاده می‌کنه. چون اجراییه ِ ‏‎main‎‏، دستورالعمل ِ پیش‌فرض‌ِه، ما هم از اینجا به بعد در خیلی از فایل‌هایی که درست می‌کنیم، قرارش میدیم. در یکی از فصل‌های آینده، این مطالبی که گفتیم رو با جزئیات بیشتری بررسی می‌کنیم.

یه فایل دیگه درست کنیم:

-- print2.hs
module Print2 where

main :: IO ()
main = do
  putStrLn "Count to four for me:"
  putStr   "one, two"
  putStr   ", three, and"
  putStrLn "four!"

گرامر ِ ‏‎do‎‏، یه گرامر ِ خاص‌ه، که برای تسلسل ِ اجراییه‌ها کاربرد داره؛ و عموماً برای متسلسل کردن ِ اجراییه‌هایی که برنامه رو تشکیل میدن (و بعضی‌هاشون الزاماً یه اثر، مثل چاپ به صفحه، رو هم اجرا می‌کنند) استفاده میشه. به همین خاطر، تایپِ ‏‎main‎‏ باید ‏‎IO ()‎‏ باشه. این نوشتارِ do واجب نیست، ولی خیلی خواناتر از کدِ معادلِ‌شه.

کد بالا رو که اجرا کنید:

Prelude> :l print2.hs
[1 of 1] Compling Print2
Ok, modules loaded: Print2.
*Print2> main
Count to four for me:
one, two, three, four! 
*Print2>

برای سرگرمی، ‏‎putStr‎‏ها و ‏‎putStrLn‎‏ها رو با هم جابجا کنین و نتیجه‌ها رو ببینین. ‏‎Ln‎‏ در ‏‎putStrLn‎‏ یعنی بعد از چاپِ متن، خط ِ جدیدی شروع می‌کنه.

الحاق ِ نوشته‌ها

الحاق کردن، یعنی به هم زنجیر‌کردن؛ و در برنامه‌نویسی، معمولاً برای تسلسل‌های خطی مثل لیست‌ها و نوشته‌ها بیان میشه. اگه نوشته‌های ‏‎"Curry"‎‏ و ‏‎" Rocks!"‎‏ رو به هم الحاق کنیم، حاصل‌ش میشه ‏‎"Curry Rocks!"‎‏. به اون فاصله ِ اولِ ‏‎" Rocks!"‎‏ دقت کنین. بدونِ اون فاصله، جواب میشه ‏‎"CurryRocks!"‎‏.

کد زیر رو تو یه فایل جدید بنویسیم:

-- print3.hs
module Print3 where

myGreeting :: String
myGreeting = "hello" ++ " world!"

hello :: String
hello = "hello"

world :: String
world = "world!"

main :: IO ()
main = do
  putStrLn myGreeting
  putStrLn secondGreeting
  where secondGreeting =
          concat [hello, " ", world]

با استفاده از ‏‎::‎‏ تایپِ همه‌ی بیانیه‌های سطح بالا رو تعیین کردیم. البته کامپایلر خودش می‌تونه تایپ‌ها رو استنتاج کنه* و نیازی به نوشتنِ تایپ سیگنچرِ بیانیه‌ها نیست، با این حال، عادت به نوشتن اونها برای برنامه‌های بزرگتر مفیده.

*

م. به این قابلیتِ کامپایلر، type inference گفته میشه.

گفتیم ‏‎String‎‏ و ‏‎[Char]‎‏ مترادف تایپی‌اند. می‌تونین با تغییرِ تایپ سیگنچرها در مثالِ آخر، و اجرای برنامه‌تون، امتحان کنین ببینین چیزی تغییر می‌کنه یا نه.

اگه برنامه رو اجرا کنیم:

Prelude> :l print3.hs
[1 of 1] Compling Print3
Ok, modules loaded: Print3.
*Print3> main
hello world!
hello world!
*Print3>

در این مثالِ ساده، چندتا چیز رو نشون دادیم:

۱.

مقادیر رو در سطحِ بالا ِ برنامه تعریف کردیم: ‏‎myGreeting‎‏، ‏‎hello‎‏، ‏‎world‎‏، و ‏‎main‎‏. منظور اینه که اونها در کلِ ماژول قابل دسترسی بودند.

۲.

تایپِ تعاریفِ سطح بالا رو صراحتاً مشخص کردیم.

۳.

نوشته‌ها رو با ‏‎(++)‎‏ و ‏‎concat‎‏ به هم الحاق کردیم.