۱۳ - ۷تعاملی‌سازی برنامه

حالا می‌خوایم کاری کنیم که برنامه اول اسم ِتون رو بپرسه، بعد هم بهتون سلام کنه. اول تابعِ ‏‎sayHello‎‏ رو با یه آرگومان بازنویسی می‌کنیم:

sayHello :: String -> IO ()
sayHello name = do
  putStrLn ("Hi " ++ name ++ "!")

دقت کنین که الحاق ِ ‏‎String‎‏ها داخل پرانتز، آرگومانِ ‏‎putStrLn‎‏ ِه.

بعد هم ‏‎main‎‏ رو طوری تغییر میدیم که اسم ِ کاربر رو بگیره:

-- src/Main.hs

main :: IO ()
main = do
  name <- getLine
  sayHello name
  dogs

چندتا چیز جدید‌اند. اینجا از گرامر ِ do استفاده کردیم که در واقع شکر گرامری ِه. از ‏‎do‎‏ داخلِ توابعیِ که ‏‎IO‎‏ برمی‌گردونن استفاده می‌کنیم تا عوارضِ جانبی رو با یه گرامر ِ مناسب متسلسل کنیم. کُدِ بالا رو جزبه‌جز بررسی می‌کنیم:

 main :: IO ()
 main = do
--     [1]
   name  <-   getLine
-- [4]  [3]     [2]
   sayHello name
--           [5]
   dogs
-- [6]

۱.

کلیدواژه ِ ‏‎do‎‏، بلوک ِ ‏‎do‎‏ رو شروع می‌کنه.

۲.

‏‎getLine‎‏ تایپِ ‏‎IO String‎‏ داره، چون برای بدست آوردنِ ‏‎String‎‏ باید I/O (ورودی/خروجی، عوارض جانبی) انجام بده. همون چیزیه که میذاره اسم ِتون رو بنویسین تا در تابعِ ‏‎main‎‏ استفاده بشه.

۳.

علامتِ ‏‎<-‎‏ در یه بلوک ِ ‏‎do‎‏، انقیاد یا بایند خونده میشه. در فصل‌هایی که از ‏‎Monad‎‏ و ‏‎IO‎‏ صحبت می‌کنیم، توضیح میدیم که این چی هست و چطور کار می‌کنه.

۴.

نتیجه‌ی بایند کردن (‏‎<-‎‏) از ‏‎IO String‎‏، تایپِ ‏‎String‎‏ میده. که اون رو به متغیرِ ‏‎name‎‏ مقیّد کردیم. به خاطر داشته باشین که ‏‎getLine‎‏ تایپ‌ش ‏‎IO String‎‏، و ‏‎name‎‏ تایپ‌ش ‏‎String‎‏ ِه.

۵.

تابعِ ‏‎sayHello‎‏ انتظار یه آرگومانِ ‏‎String‎‏ داره، که ‏‎name‎‏ چنین تایپی داره، اما ‏‎getLine‎‏ اینطور نیست.

۶.

‏‎dogs‎‏ انتظارِ هیچ چیز رو نداره.* یه اجراییه ِ ‏‎IO‎‏ با تایپِ ‏‎IO ()‎‏ هست که با تایپِ کلیِ ‏‎main‎‏ سازگاره.

*

مثل سگ‌های واقعی.

حالا میسازیم:

$ stack build

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

$ stack exec hello

بعد از اینکه دکمه‌ی enter رو میزنین، برنامه منتظرِ ورودی ِ شما میشه. خطِ ترمینال چشمک میزنه و منتظر میشه تا اسم ِتون رو وارد کنین. اسم ِتون رو که بنویسین و enter بزنین، بهتون سلام می‌کنه و قربون صدقه‌ی یه هاپو میره.

اگه ‏‎getLine‎‏ رو به ‏‎sayHello‎‏ میدادیم چی؟

اگه ‏‎main‎‏ رو بدون گرامر ِ ‏‎do‎‏ می‌نوشتیم، یعنی بدونِ ‏‎<-‎‏ مثلِ زیر

main :: IO ()
main = sayHello getLine

خطای تایپ ِ زیر رو می‌گرفتیم:

$ stack build
[2 of 2] Compiling Main

src/Main.hs:8:17:
Couldn't match type ‘IO String’ with ‘[Char]’
Expected type: String
  Actual type: IO String
In the first argument of ‘sayHello’,
  namely ‘getLine’
In the expression: sayHello getLine

دلیل‌ش اینه که ‏‎getLine‎‏ یه اجراییه ِ ‏‎IO‎‏ با تایپِ ‏‎IO String‎‏ ِه، اما ‏‎sayHello‎‏ انتظارِ یه مقدار با تایپِ ‏‎String‎‏ داره. باید با استفاده از ‏‎<-‎‏، از روی ‏‎IO‎‏ بایند کنیم تا نوشته ای که برای ‏‎sayHello‎‏ می‌خوایم رو بگیریم. اینها رو با جزئیات بیشتر توضیح میدیم – در یکی از فصل‌ها یه کم توضیح میدیم، در یکی دیگه از فصل‌ها خیلی بیشتر.

اضافه کردنِ prompt

میشه با اضافه کردنِ یه prompt که به کاربر بگه برنامه منتظرِ ورودی‌ه، استفاده از برنامه رو راحت‌تر کرد! باید ‏‎main‎‏ رو تغییر بدیم:

module Main where

import DogsRule
import Hello
import System.IO

main :: IO ()
main = do
  hSetBuffering stdout NoBuffering
  putStr "Please input your name: "
  name <- getLine
  sayHello name
  dogs

خیلی کارها کردیم. یکی اینکه بجای ‏‎putStrLn‎‏ از ‏‎putStr‎‏ استفاده کردیم تا ورودی جلوی prompt بیاد. ‏‎System.IO‎‏ رو هم وارد کردیم تا از ‏‎hSetBuffering‎‏، ‏‎stdout‎‏، و ‏‎NoBuffering‎‏ استفاده کنیم. اون خط اول در بلوک ِ ‏‎do‎‏ کاری می‌کنه که ‏‎putStr‎‏ بافِر نشه (به تعویق نیوفته) و بی‌وقفه چاپ شه.* اگه برنامه رو دوباره بسازین و اجرا کنین، اینطور کار می‌کنه:

$ stack exec hello
Please input your name: julie
Hi julie!
Who's a good puppy?!
YOU ARE!!!!!
*

م. به طور پیش‌فرض، ‏‎LineBuffering‎‏ حالتِ بافِر شدنِ ‏‎stdout‎‏ ِه، یعنی خروجی‌ها رو تا زمانی که یه خط جدید (‏‎\n‎‏) نبینه، بافِر می‌کنه (در حافظه‌ی میانجی نگه میداره). ‏‎NoBuffering‎‏ این بافِر شدن رو (در صورت امکان) لغو می‌کنه.

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