۲۲ - ۷استخدام برای برنامهنویسی با یه کلکِ عجیب
بعضی شرکتها برای فیلترکردن (نه برای تستکردن) متقاضیهای شغلهای نرمافزاری، از FizzBuzz استفاده میکنن. صورت مسئله اینه:
برنامهای بنویسین که اعداد ۱ تا ۱۰۰ رو چاپ میکنه. اما بجای مضارب ۳ چاپ کنه “Fizz” و بجای مضارب ۵ چاپ کنه “Buzz”. برای اعدادی که هم مضرب ۳ هستن و هم مضرب ۵، چاپ کنه “FizzBuzz”.
یه راه حل معمولی با هسکل میتونه این شکلی باشه:
fizzBuzz :: Integer -> String
fizzBuzz n | n `mod` 15 == 0 = "FizzBuzz"
| n `mod` 5 == 0 = "Buzz"
| n `mod` 3 == 0 = "Fizz"
| otherwise = show n
main :: IO ()
main =
mapM_ (putStrLn . fizzBuzz) [1..100]با استِیت FizzBuzz نوشتن تنبیه خوبیه! ببینیم با State چطور میشه:
import Control.Monad
import Control.Monad.Trans.State
fizzBuzz :: Integer -> String
fizzBuzz n | n `mod` 15 == 0 = "FizzBuzz"
| n `mod` 5 == 0 = "Buzz"
| n `mod` 3 == 0 = "Fizz"
| otherwise = show n
fizzbuzzList :: [Integer] -> String
fizzbuzzList list =
execState (mapM_ addResult list) []
addResult :: Integer -> State [String] ()
addResult n = do
xs <- get
let result = fizzBuzz n
put (result : xs)دقت کنین که State در واقع یه تایپ مستعار برای StateT که وارد کردینه:
main :: IO ()
main =
mapM_ putStrLn $
reverse $ fizzbuzzList [1..100]قسمت خوبش اینه که قبل از دادن جوابها به خروجی استاندارد (به واسطهی putStrLn)، اول دادهها رو جمعآوری میکنیم. بدیش اینه که یه لیست رو معکوس میکنیم. معکوس کردنِ لیستهای تک-پیوندی خیلی خوب نیست، حتی تو هسکل؛ با لیستِ بینهایت هم به جواب نمیرسه. یه موردِ دیگه، ورودیای میگیریم که اعدادِ مورد استفاده برای FizzBuzz رو به صورت خطی از اول تا انتها تعیین میکنه.
دو راه برای مدیریت ِ این وجود داره. یکی استفاده از نوعدادهایه که اضافه کردنِ المان به انتهاش کم هزینهتر باشه. استفاده از (++) به صورتِ بازگشتی ممکنه خیلی سنگین بشه، پس از یه چیزی استفاده کنیم که با زمانِ ثابت به انتها المان اضافه کنه. لیستِ دیفرنس با O(1) المانها رو به انتها اضافه میکنه.
import Control.Monad
import Control.Monad.Trans.State
-- http://hackage.haskell.org/package/dlist
import qualified Data.DList as DL
fizzBuzz :: Integer -> String
fizzBuzz n | n `mod` 15 == 0 = "FizzBuzz"
| n `mod` 5 == 0 = "Buzz"
| n `mod` 3 == 0 = "Fizz"
| otherwise = show n
fizzbuzzList :: [Integer] -> [String]
fizzbuzzList list =
let dlist =
execState (mapM_ addResult list)
DL.empty
-- برگشتن به لیست معمولی
in DL.apply dlist []
addResult :: Integer
-> State (DL.DList String) ()
addResult n = do
xs <- get
let result = fizzBuzz n
-- که به اول اضافه میکنه، cons برخلاف
-- میکنه. append به انتها snoc
put (DL.snoc xs result)
main :: IO ()
main =
mapM_ putStrLn $ fizzbuzzList [1..100]اگه GHC ۷٫۱۰ یا جدیدتر دارین، mapM_ با هر Foldable ای کار میکنه (نه فقط لیست). اونطوری میشه یه کم تمیزتر نوشت:
Prelude> :t mapM_
mapM_ :: (Monad m, Foldable t)
=> (a -> m b) -> t a -> m ()اگه تبدیلِ به لیست رو بسپاریم به نمونه ِ Foldable ِ تایپِ DList، میتونیم مقداری کُد رو حذف کنیم:
fizzbuzzList :: [Integer]
-> DL.DList String
fizzbuzzList list =
execState (mapM_ addResult list) DL.empty
addResult :: Integer
-> State (DL.DList String) ()
addResult n = do
xs <- get
let result = fizzBuzz n
put (DL.snoc xs result)
main :: IO ()
main =
mapM_ putStrLn $ fizzbuzzList [1..100]نمونه ِ Foldable ِ تایپِ DList به خاطر محدودیتهایی که این نوعداده داره، قبل از فولدکردن تبدیل به لیست میشه. افزودن به انتها ِ ارزون داریم، ولی نمیشه قبل از درستکردنِ کلِ ساختار "ببینیم" چی درست شده. این مبحث رو با جزئیات بیشتر تو فصلِ بعد توضیح میدیم.
شاید به نظرتون اومده باشه که اینجا استفاده از State یه کم اضافی بود. حق دارین! برای چنین کاربردهایی تو هسکل، State رایج نیست. شاید برای بهینهسازیِ انتخابی از یه نوعِ دیگه State به اسمِ ST استفاده کنین، ولی State بهخودیِخود یه کم سلیقهایه. در استفاده، یا استفاده نکردن از State احساسِ هیچ فشاری نکنین. لطفاً چندتا مصاحبهگرِ استخدام رو با اون FizzBuzz ِ عجیب بترسونین. از اونی که نشون دادیم هم پیچیدهتر بنویسین!
FizzBuzz متفاوت
یه تمرینه! بجای اینکه ساختار داده رو تغییر بدین، اون مشکلِ معکوس کردنِ لیست رو اینطوری درست کنین:
fizzbuzzFromTo :: Integer
-> Integer
-> [String]
fizzbuzzFromTo = undefinedباز هم توی لیستِ خروجی cons کنین، اما شمارش رو برعکس شروع کنین تا ترتیبشون درست دربیاد. این روش برای حذفِ reverse های اضافی رایجتره.