۹ - ۱۲تمرین‌های فصل

بخش اول این تمرین‌ها بیشتر جنبه‌ی دوره‌ای داره، اما چندتا چیز جدید هم معرفی می‌کنه. بخش دوم ممکنه یه کم چالش برانگیز باشه، اما از هیچ مبحث یا گرامر ای که قبلاً نگفته باشیم استفاده نمی‌کنه. اگه به مشکل خوردین، شاید با دوره‌ی بخش مربوطه مشکل‌تون برطرف شه.

‏‎Data.Char‎‏

این چندتا تمرینِ اول ساده‌اند، اما تابع‌های جدیدی از کتابخونه‌ها معرفی، و بعضی از چیزهایی که قبلاً یاد گرفتیم رو دوره می‌کنن. بعضی از توابعی که اینجا به کار بردیم در ‏‎Prelude‎‏ نیستن و باید از ‏‎Data.Char‎‏ وارد ِشون کنیم. این کار رو با جمله‌ی ‏‎import Data.Char‎‏ میشه انجام داد، چه اولِ یه فایل بنویسین (پیشنهاد ما) چه در REPL. این کار تعدادی تابع که با تایپ‌های ‏‎Char‎‏ و ‏‎String‎‏ کار می‌کنن رو واردِ گستره می‌کنه.

۱.

تایپ‌های ‏‎isUpper‎‏ و ‏‎toUpper‎‏ رو استعلام کنین.

۲.

با توجه به مثال‌های زیر، از کدوم استفاده کنیم تا بتونیم با تابعی، حروف بزرگ رو از یه ورودیِ ‏‎String‎‏ فیلتر کنیم؟ یعنی مثلاً با ورودیِ ‏‎“HbEfLrLxO”‎‏، تابع باید خروجیِ ‏‎“HELLO”‎‏ بده.

Prelude Data.Char> isUpper 'J'
True
Prelude Data.Char> toUpper 'j'
'J'

۳.

تابعی بنویسین که حرف اولِ یه نوشته رو بزرگ کنه و نوشته رو تماماً برگردونه. مثلاً با آرگومانِ ‏‎“julie”‎‏ جواب ‏‎“Julie”‎‏ بشه.

۴.

حالا یه نسخه‌ی بازگشتی از اون تابع رو بنویسین که اگه بهِش ورودیِ ‏‎“woot”‎‏ دادین، سَرِتون داد بزنه ‏‎“WOOT”‎‏. تایپ سیگنچر تغییری نمی‌کنه، اما یه فرمان پایه لازم دارین.

۵.

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

۶.

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

رمزنگاری

برای این تمرین به استفاده از ‏‎Data.Char‎‏ ادامه میدیم. این تمرین‌ها رو تو یه ماژول به اسمِ ‏‎Cipher‎‏ ذخیره کنین، چون در فصل‌های بعد لازم‌شون داریم. فعلاً یه رمزنگارِ سزار تعریف می‌کنین، اما فصل‌های بعد قابلیت‌هایی رو پیشنهاد می‌کنیم که بهش اضافه کنین.

رمزنگارِ سزار، یک رمزنگارِ جایگزینی ِ ساده‌ست که در اون، هر حرف با یه حرفی که فاصله‌ی ثابتی ازش در الفبا داره جایگزین میشه. حالت‌های متنوعی از این رمزنگاری وجود داره – میشه حروف قبل‌تر یا بعدتر رو با هر تعدادی جایگزین کرد. مثلاً با ۳ حرف جابجایی به راست، ‏‎‘A’‎‏ تبدیل میشه به ‏‎‘D’‎‏، و ‏‎‘B’‎‏ به ‏‎‘E’‎‏ تبدیل میشه. یا با ۵ حرف جابجایی به چپ، ‏‎‘a’‎‏ به ‏‎‘v’‎‏ تبدیل میشه، و الی آخر.

هدف این تمرین نوشتن یک رمزنگارِ سزار ِه که حروف رو به راست منتقل می‌کنه. می‌تونین اول مقدار جابجایی رو ثابت در نظر بگیرین، بعد تابعی بنویسین که میزان جابجایی یکی از آرگومان‌هاش باشه. کمی سخت‌تره اما میشه هر دفعه پیغام‌های سِرّی‌مون رو متفاوت رمزگذاری کنیم.

روی اینترنت رمزنگارهای سزار که با هسکل نوشته شده باشن خیلی زیادن. ما هم چندتا نکته برای راهنمایی میگیم که کمتر وسوسه‌ی دیدن جواب از روی اینترنت بشین. اما بعد از اینکه خودتون حل کردین، بد نیست جواب‌تون رو با اونهایی که رو اینترنت هستن مقایسه کنین.

خط اول کُدتون باید اینطور باشه:

module Cipher where

import Data.Char

دو تابعِ ‏‎ord‎‏ و ‏‎chr‎‏ در ‏‎Data.Char‎‏ هستن، که معادلِ ‏‎Int‎‏ ِ یک ‏‎Char‎‏ رو بر مبنای سیستمِ یونیکُد، و برعکس‌ش (یعنی معادلِ ‏‎Char‎‏ برای یه ‏‎Int‎‏) رو پیدا می‌کنن:

*Cipher> :t chr
chr :: Int -> Char
*Cipher> :t ord
ord :: Char -> Int

استفاده از این دو تابع اختیاریه؛ از راه‌های دیگه‌ای هم میشه حروف رو منتقل کرد، اما استفاده از ‏‎chr‎‏ و ‏‎ord‎‏ ممکنه حل رو ساده‌تر کنه.

انتقال حروف در آخرِ الفبا باید برگرده از اول، مثلاً اگه انتقال ۳ حرفی به راست از ‏‎‘z’‎‏ بدین، باید به حرف ‏‎‘c’‎‏ برسین، نه یه جا وسطِ جنگلِ یونیکُد. این کار، بسته به اینکه چطور کُدتون رو نوشتین، ممکنه سخت باشه. شاید خوب باشه از یه حرفِ پایه (مثلاً ‏‎‘a’‎‏) شروع کنین، و با استفاده از ‏‎mod‎‏ مطمئن باشین که فقط بینِ ۲۶ حرف انگلیسی جابجا میشین.

یه تابع هم در کنارِ این تابع تعریف کنین، به اسمِ ‏‎unCaesar‎‏ که نوشته‌ی ورودی رو رمزگشایی می‌کنه. در یکی از فصل‌های آینده، تست‌ش می‌کنیم.

توابع استانداردِ خودتون رو بنویسین

اینجا چندتا از توابعِ استاندارد رو توضیح دادیم و شما باید نسخه‌ی خودتون از اونها رو بنویسین. این تمرین کمک می‌کنه درک عمیق‌تری از خوداتکایی روی لیست‌ها، و نحوه‌ی افزایشِ انعطاف‌پذیریِ توابع با تنوع دادن به ورودی‌هاشون پیدا کنین. اگه بگردین جوابِ این تمرین‌ها هم رو اینترنت پیدا میشه، ولی شما اهل این کارا نیستین، چون نمی‌خواین خودتون رو گول بزنین. مگه نه؟

با یه مثال نشون میدیم که دنبال چی هستیم. تابعِ ‏‎and‎‏ یه لیستِ ‏‎Bool‎‏ می‌گیره* و اگر و فقط اگر هیچ کدوم از مقادیرِ لیست ‏‎False‎‏ نباشن، ‏‎True‎‏ برمی‌گردونه. نسخه‌ی خودتون رو میشه اینطور بنویسین:

-- (&&) خوداتکاییِ مستقیم، بدون استفاده از
myAnd :: [Bool] -> Bool
myAnd [] = True
myAnd (x:xs) =
  if x == False
  then False
  else myAnd xs

-- (&&) مستقیم، با استفاده از recursion
myAnd :: [Bool] -> Bool
myAnd [] = True
myAnd (x:xs) = x && myAnd xs
*

دقت کنین اگه از 7.10 GHCi یا جدیدتر استفاده می‌کنین، توابعِ ‏‎and‎‏، ‏‎any‎‏، و ‏‎all‎‏ انتزاعی‌تر شدن و علاوه بر لیست‌ها، با هر نوع‌داده‌ای که یه نمونه از تایپکلاسِ ‏‎Foldable‎‏ داشته باشه کار می‌کنن. با خیال راحت ادامه بدین چون بعداً به ‏‎Foldable‎‏ می‌رسیم.

حالا خوش بگذرونیم:

۱.

تابعِ ‏‎myOr‎‏، اگه حداقل یک مقدارِ ‏‎Bool‎‏ در لیستِ ورودی ‏‎True‎‏ باشه، باید ‏‎True‎‏ برگردونه.

myOr :: [Bool] -> Bool
myOr = undefined

۲.

تابعِ ‏‎myAny‎‏ در صورتی ‏‎True‎‏ بر می‌گردونه که از اعمال ِ ‏‎a -> Bool‎‏ به حداقل یکی از المان‌های لیست، جوابِ ‏‎True‎‏ بدست بیاد.

myAny :: (a -> Bool) -> [a] -> Bool
myAny = undefined

چندتا مثال برای تست کردنِ ‏‎myAny‎‏:

Prelude> myAny even [1, 3, 5]
False
Prelude> myAny odd [1, 3, 5]
True

۳.

بعد از نوشتن تابعِ بازگشتی ِ ‏‎myElem‎‏، یه نسخه‌ی دیگه ازش بنویسین که از ‏‎any‎‏ استفاده کنه. تابعِ ‏‎elem‎‏ که در 7.10 GHCi و جدیدتر تعریف شده، بجای اینکه فقط با لیست کار کنه، با ‏‎Foldable‎‏ کار می‌کنه. می‌تونین اون رو نادیده بگیرین و نسخه‌ی خودتون رو فقط برای لیست بنویسین.

myElem :: Eq a => a -> [a] -> Bool
myElem = undefined
Prelude> myElem 1 [1..10]
True
Prelude> myElem 1 [2..10]
False

۴.

تابعِ ‏‎myReverse‎‏ رو تعریف کنین.

myReverse :: [a] -> [a]
myReverse = undefined
Prelude> myReverse "blah"
"halb"
Prelude> myReverse [1..5]
[5,4,3,2,1]

۵.

تابعِ ‏‎squish‎‏ یه لیستِ لیست رو به یک لیست تبدیل می‌کنه.

squish :: [[a]] -> [a]
squish = undefined

۶.

تابعِ ‏‎squishMap‎‏ یه تابع رو به یک لیست نگاشت میده و نتیجه‌هاشون رو الحاق می‌کنه.

squishMap :: (a -> [b]) -> [a] -> [b]
squishMap = undefined
Prelude> squishMap (\x -> [1, x, 3]) [2]
Prelude> [1,2,3]
Prelude> :{
Prelude| squishMap
Prelude|   (\x -> "WO "++[x]++" HOO ") "123"
Prelude| :}
"WO 1 HOO WO 2 HOO WO 3 HOO "

۷.

تابعِ ‏‎squishAgain‎‏ یه لیستِ لیست رو به یک لیست لِه می‌کنه. این بار از تابعِ ‏‎squishMap‎‏ استفاده کنین.

squishAgain :: [[a]] -> [a]
squishAgain = undefined

۸.

تابعِ ‏‎myMaximumBy‎‏ یک تابعِ مقایسه و یک لیست می‌گیره، و بر مبنای آخرین مقداری که از مقایسه خروجیِ ‏‎GT‎‏ میده، بزرگترین المان لیست رو برمی‌گردونه. اگه ‏‎maximumBy‎‏ رو از ‏‎Data.List‎‏ وارد کنین و تایپ‌ش رو استعلام کنین، بجای تایپِ:

(a -> a -> Ordering) -> [a] -> a

چنین تایپی می‌بینین:

   Foldable t
=> (a -> a -> Ordering) -> t a -> a

شما هم می‌تونین با ‏‎[]‎‏ بنویسین:

myMaximumBy :: (a -> a -> Ordering)
            -> [a] -> a
myMaximumBy = undefined
Prelude> let xs = [1, 53, 9001, 10]
Prelude> myMaximumBy compare xs
9001

۹.

تابعِ ‏‎myMinimumBy‎‏ یک تابعِ مقایسه و یک لیست می‌گیره، و بر مبنای آخرین مقداری که از مقایسه خروجی ‏‎LT‎‏ میده، کوچکترین المان لیست رو برمی‌گردونه.

myMinimumBy :: (a -> a -> Ordering)
            -> [a] -> a
myMinimumBy = undefined
Prelude> let xs = [1, 53, 9001, 10]
Prelude> myMinimumBy compare xs
1

۱۰.

با توابعِ ‏‎myMinimumBy‎‏ و ‏‎myMaximumBy‎‏، تابع‌های ‏‎minimum‎‏ و ‏‎maximum‎‏ ِ خودتون رو بنویسین. مثل خیلی از تابع‌هایی که بالاتر دیدیم، این دو تا تابع هم در 7.10 GHCi یا جدیدتر، بجای لیست، با همه‌ی نوع‌سازهایی که یه نمونه از تایپکلاسِ ‏‎Foldable‎‏ دارن کار می‌کنن.

myMaximum :: (Ord a) => [a] -> a
myMaximum = undefined

myMinimum :: (Ord a) => [a] -> a
myMinimum = undefined