۷ - ۱۰نمایش ترکیب

اگه از فصل ۳ یادتون باشه، گفتیم که تابع‌های ‏‎print‎‏ و ‏‎putStr‎‏ در ظاهر مشابه‌اند، اما به خاطرِ تایپ متفاوت‌شون رفتار متفاوتی هم دارن. حالا دقیق‌تر به این مورد نگاه می‌کنیم.

اول اینکه، ‏‎putStrLn‎‏ و ‏‎putStr‎‏ تایپِ یکسان دارن:

putStr :: String -> IO ()
putStrLn :: String -> IO ()

اما تایپِ ‏‎print‎‏ فرق داره:

print :: Show a => a -> IO ()

همه‌شون جواب با تایپِ ‏‎IO ()‎‏ میدن، که دلایل‌ش رو در فصلِ قبل توضیح دادیم. ولی پارامترهاشون فرق می‌کنن. دو تابعِ اول ‏‎String‎‏ می‌گیرن، اما ‏‎print‎‏ یک پارامتر با پلی‌مورفیسمِ محدود ِ ‏‎Show a => a‎‏ داره. دو تابعِ اول برای نمایشِ مقادیری که به خودیِ‌خود ‏‎String‎‏ هستن مناسب‌اند. ولی اعداد (یا هر مقدارِ غیرِ نوشته) رو چطور نشون بدیم؟ اول باید به نوشته تبدیل‌شون کنیم، بعد می‌تونیم اون نوشته‌ها رو چاپ کنیم.

شاید از توضیحات‌مون درباره‌ی تایپکلاسِ ‏‎Show‎‏، تابعِ ‏‎show‎‏ یادتون باشه. تایپ‌ش این بود:

show :: Show a => a -> String

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

در زیر دو راه برای نوشتنِ تابعِ ‏‎print‎‏ با ‏‎putStrLn‎‏ و ‏‎show‎‏ رو آوردیم:

print :: Show a => a -> IO ()
print a = putStrLn (show a)

-- استفاده از اوپراتورِ . برای
-- ترکیب توابع
(.) :: (b -> c) -> (a -> b) -> a -> c

-- رو اینطوری هم میشه تعریف کرد print
print :: Show a => a -> IO ()
print a = (putStrLn . show) a

حالا این کاربردِ ‏‎(.)‎‏، ‏‎putStrLn‎‏، و ‏‎show‎‏ رو مرحله به مرحله بررسی کنیم:

(.) :: (b -> c) -> (a -> b) -> a -> c

putStrLn :: String -> IO ()
--            [1]      [2]

show            :: Show a => a -> String
--                     [3]          [4]

putStrLn . show :: Show a => a -> IO ()
--                 [5]            [6]

(.) :: (b -> c) -> (a -> b) -> a -> c
--     [1]  [2]    [3]  [4]   [5]  [6]

-- اگه متغیرهای تایپی رو با تایپ‌های
-- :مختصِ این کاربردِ (.) جایگزین کنیم

(.) :: Show a => (String -> IO ())
              -> (a -> String)
              -> a -> IO ()

(.) :: (b      -> c)
    -- (String -> IO ())

    -> (a -> b)
    -- (a -> String)

    -> a -> c
    -- a -> IO ()

۱.

نوشته‌‌ای که ‏‎putStrLn‎‏ به عنوانِ آرگومان قبول می‌کنه.

۲.

‏‎IO ()‎‏ که ‏‎putStrLn‎‏ برمی‌گردونه، یعنی اثر جانبی (که همون چاپ هست) رو پیاده می‌کنه و مقدارِ واحد رو برمی‌گردونه.

۳.

تایپ ‏‎a‎‏ که باید تایپکلاس ‏‎Show‎‏ رو داشته باشه؛ این همون ‏‎Show a => a‎‏ از تابع ‏‎show‎‏ ِه (یکی از متودهای تایپکلاس ‏‎Show‎‏).

۴.

این نوشته‌ایه که ‏‎show‎‏ برمی‌گردونه. همون چیزیه که مقدارِ ‏‎Show a => a‎‏ بهش تبدیل میشه.

۵.

تایپِ ‏‎Show a => a‎‏ ی که تابعِ ترکیب‌شده ِ نهایی انتظار داره.

۶.

‏‎IO ()‎‏ ای که تابعِ ترکیب‌شده ِ نهایی برمی‌گردونه.

حالا می‌تونیم به سبکِ بی‌نقطه بنویسیم‌ش. وقتی بیشتر با ترکیب ِ توابع کار داریم تا اعمال ِشون، ممکنه نسخه‌ی بی‌نقطه گاهی اوقات (نه همیشه) خوشایندتر باشه.

این نسخه‌ی قبلیِ تابع:

print :: Show a => a -> IO ()
print a = (putStrLn . show) a

و این هم نسخه‌ی بی‌نقطه از ‏‎print‎‏:

print :: Show a => a -> IO ()
print = putStrLn . show

هدفِ اصلیِ ‏‎print‎‏، ترکیب ِ ‏‎putStrLn‎‏ و ‏‎show‎‏ ِه تا مجبور نباشیم تابعِ ‏‎show‎‏ رو خودمون صدا بزنیم. به کلامِ دیگه، ‏‎print‎‏ اساساً با ترکیب توابع سروکار داره، به همین خاطر هم به سبک بی‌نقطه تابعِ خوبی شد.