۱۱ - ۱۳ساخت و تخریب مقادیر
اساساً با مقادیر دو کار میشه انجام داد: میشه اونها رو ایجاد کرد یا ساخت، یا میشه تطبیقشون بدیم و مصرف ِشون کنیم. بالاتر گفتیم که چرا به دادهسازها و نوعسازها میگیم سازنده، در این بخش هم این موضوع رو ادامه میدیم و نحوهی ساختن ِ مقادیرِ تایپهای مختلف رو توضیح میدیم. در فصلهای قبل هم این کارها رو انجام دادین، ولی امیدواریم این بخش درک عمیقتری بهتون بده.
ساخت و تخریب ِ مقادیر یه جور دوگانی رو تشکیل میدن. دادهها در هسکل تغییرناپذیر اند، پس در واقع "نحوهی ساخته شدن مقادیر" به همراه خودشون ذخیره میشه. از این اطلاعات میشه در مصرف یا تخریب ِ مقادیر استفاده کرد.
با تعریفِ چند نوعداده شروع میکنیم:
data GuessWhat =
ChickenHut deriving (Eq, Show)
data Id a =
MkId a deriving (Eq, Show)
data Product a b =
Product a b deriving (Eq, Show)
data Sum a b =
First a
| Second b
deriving (Eq, Show)
data RecordProduct a b =
RecordProduct { pfirst :: a
, psecond :: b }
deriving (Eq, Show)
حالا که چند نوع تایپ داریم، شروع به ساخت ِ مقادیر از اون تایپها میکنیم.
Sum
و Product
اینجا از Sum
و Product
برای یه جور ارائهی کلی از تایپهای جمع و ضرب استفاده کردیم. در هسکلنویسیِ معمولی، معمولاً جمعها یا ضربهایی که قابلیتِ تودرتو شدن رو داشته باشن لازم نمیشه، مگر اینکه روی پروژهی پیشرفتهای کار کنین. اینجا فقط برای نشون دادن استفاده کردیم.
اگه دو مقدار در یه ضرب هستن، انتقالِ اونها به Product
کارِ سادهایه (نکته: برای همهی مثالهای زیر، تعاریفِ Sum
و Product
باید در گستره باشن):
newtype NumCow =
NumCow Int
deriving (Eq, Show)
newtype NumPig =
NumPig Int
deriving (Eq, Show)
newtype Farmhouse =
Farmhouse NumCow NumPig
deriving (Eq, Show)
type Farmhouse' = Product NumCow NumPig
Farmhouse
و Farmhouse'
یکساناند.
برای یه مثال با سه مقدارِ ضرب بجای دوتا، میشه از تایپِ Product
استفاده کرد، اما آرگومان دومش باید دوباره از تایپِ Product
باشه. در واقع هر چقدر دلتون بخواد میتونین این تودرتو کردن رو ادامه بدین:
newtype NumSheep =
NumSheep Int
deriving (Eq, Show)
newtype BigFarmhouse =
BigFarmhouse NumCow NumPig NumSheep
deriving (Eq, Show)
type Farmhouse' =
Product NumCow (Product NumPig NumSheep)
یه کارِ مشابه هم میشه با Sum
انجام داد:
type Name = String
type Age = Int
type LovesMud = Bool
گوسفندها در سال بین ۰٫۹ تا ۱۳ کیلو (۲ تا ۳۰ پوند) پشم تولید میکنن! گوسفندهای ایسلندی به اندازهی بقیهی گونهها پشم تولید نمیکنن، اما پشم بهتری میدن.
type PoundsOfWool = Int
data CowInfo =
CowInfo Name Age
deriving (Eq, Show)
data PigInfo =
PigInfo Name Age LovesMud
deriving (Eq, Show)
data SheepInfo =
SheepInfo Name Age PoundsOfWool
deriving (Eq, Show)
data Animal =
Cow CowInfo
| Pig PigInfo
| Sheep SheepInfo
deriving (Eq, Show)
-- راه جایگزین
type Animal' =
Sum CowInfo (Sum PigInfo SheepInfo)
در REPL با استفاده از First
و Second
روی دادهسازهای Sum
تطبیقِ الگو میکنیم:
-- درست بنویسیم
Prelude> let bess' = (CowInfo "Bess" 4)
Prelude> let bess = First bess' :: Animal'
Prelude> :{
*Main| let e' =
*Main| Second (SheepInfo "Elmer" 5 5)
*Main| :}
Prelude> let elmer = Second e' :: Animal'
-- اشتباه بنویسیم
Prelud> :{
*Main| let elmo' =
*Main| Second (SheepInfo "Elmo" 5 5)
*Main| :}
Prelude> let elmo = First elmo' :: Animal'
Couldn't match expected type ‘CowInfo’
with actual type ‘Sum a0 SheepInfo’
In the first agreement of ‘First’, namely
‘(Second (SheepInfo "Elmo" 5 5))’
In the expression:
First (Second (SheepInfo "Elmo" 5 5))
:: Animal'
CowInfo
آرگومانِ دادهساز ِ First
ِه، اما SheepInfo
در سازنده ِ Second
تودرتو شده (یعنی Second
ِ Second
ِه). در خطایی که کردیم، جهتِ تودرتو شدن رو اشتباه پیش رفتیم.
Prelude> let sheep = SheepInfo "Baaaaa" 5 5
Prelude> :t First (Second sheep)
First (Second (SheepInfo "Baaaaa" 5 5))
:: Sum (Sum a SheepInfo) b
Prelude> :info Animal'
type Animal' =
Sum CowInfo (Sum PigInfo SheepInfo)
-- Defined at code/animalFarm1.hs:61:1
همونطور که گفتیم، خودِ تایپهای Sum
و Product
در هسکلِ عادی زیاد به کار نمیرن، اما به تقویتِ درکِ این ساختار برای تایپهای جمع و ضرب کمک میکنن.
ساختن ِ مقادیر
نوعداده ِ اول که تعریف کردیم، GuessWhat
، پیشوپا افتاده و معادلِ تایپِ واحد یا ()
هست:
trivialValue :: GuessWhat
trivialValue = ChickenHut
تایپهای مشابهِ این برای مواقعی که برای نشوندادنِ مفاهیمِ گسسته ای که نمیخواین به تایپِ واحد تبدیل کنین کاربرد دارن. بعداً بیشتر توضیح میدیم که چطور چنین چیزی میتونه کُد رو مفهومتر یا انتزاعیتر کنه. اینجا هیچ گرامر ِ خاصی استفاده نکردیم. گفتیم که trivialValue
معادلِ دادهساز ِ پوچگانه ِ ChickenHut
باشه، و نتیجتاً یه مقدار از تایپِ GuessWhat
داریم.
حالا به یه نوعساز ِ یگانی نگاه میکنیم که یک دادهساز ِ یگانی داره:
data Id a =
MkId a deriving (Eq, Show)
به خاطرِ اینکه Id
آرگومان داره، اول باید به یه چیزی اعمال ِش کنیم تا بتونیم یه مقدار از اون تایپ درست کنیم:
-- نکته:
-- MkId :: a -> Id a
idInt :: Id Integer
idInt = MkId 10
میریم سراغِ تایپ ضربمون که دو آرگومان داشت. اول چندتا تایپ مترادف تعریف میکنیم که خوندنش راحتتر بشه:
type Awesome = Bool
type Name = String
person :: Product Name Awesome
person = Product "Simon" True
تایپهای مستعارِ Awesome
و Name
رو برای وضوح بیشتر نوشتیم. ما رو مجبور به تغییرِ جملات ِ کُدمون نمیکنن. میشد بجای تایپِ مترادف، از نوعداده استفاده کنیم (پایینتر برای یه تایپ جمع این کار رو میکنیم)، اما این یه راهِ سریع و بیدردسر برای ساخت مقادیری ِه که لازم داریم. دقت کنین از دادهساز ِ Product
که بالاتر نوشتیم استفاده کردیم. دادهساز ِ Product
، یه تابع با دو آرگومان ِه: Name
و Awesome
. توجه هم داشته باشین که سایمونها بیبروبرگرد باحال اند.
حالا از تایپِ Sum
که بالاتر تعریف کردیم استفاده میکنیم:
data Sum a b =
First a
| Second b
deriving (Eq, Show)
data Twitter =
Twitter deriving (Eq, Show)
data AskFm =
AskFm deriving (Eq, Show)
socialNetwork :: Sum Twitter AskFm
socialNetwork = First Twitter
اینجا تایپمون یه جمع از Twitter
یا AskFm
ِه. بدونِ ضرب نمیشه هر دو مقدار رو همزمان داشته باشیم؛ جمع برای بیانِ فصل یا داشتنِ فقط یکی از مقادیرِ ممکن به کار میره. برای بیانِ مقدارِ موردِ نظر در چنین فصلی، باید از یکی از دادهسازهایی که با تعریفِ Sum
ایجاد شدن استفاده کنیم. فرض کنین جابجا بنویسیم:
Prelude> type SN = Sum Twitter
Prelude> Seccond Twitter :: SN
Couldn't match expected type ‘AskFm’ with
actual type ‘Twitter’
In the first argument of ‘Second’,
namely ‘Twitter’
In the expression:
Second Twitter :: Sum Twitter AskFm
Prelude> First AskFm :: Sum Twitter AskFm
Couldn't match expected type ‘Twitter’ with
actual type ‘AskFm’
In the first argument of ‘First’,
namely ‘AskFm’
In the expression:
First AskFm :: Sum Twitter AskFm
تایپِ هرکدوم از دادهسازها با اعلامیهی تایپ مشخص میشن. تایپ سیگنچر ِ Sum Twitter AskFm
داره میگه که چه تایپی با دادهساز ِ First
و چه تایپی با دادهساز ِ Second
جور میشه. همین ترتیب رو میشه با تعریفِ مستقیمِ یه نوعداده مثل زیر هم داشته باشیم:
data SocialNetwork =
Twitter
| AskFm
deriving (Eq, Show)
دیگه دادهسازهای Twitter
و AskFm
مستقیماً از اعضای تایپ جمع ِ SocialNetwork
هستن، اما قبلاً عضو تایپِ Sum
بودن. ببینیم با تایپ مترادف چطور میشه:
type Twitter = String
type AskFm = String
twitter :: Sum Twitter AskFm
twitter = First "Twitter"
askfm :: Sum Twitter AskFm
askfm = First "AskFm"
مثال بالا یه مشکلی داره. اسمِ askfm
القا میکنه که منظورمون Second "AskFm"
بوده. ولی خراب نوشتیم؛ تایپ سیستم هم هیچ ایرادی نگرفت، چون بجای تعریفِ نوعداده، از تایپ مترادف استفاده کردیم. هر دو مقدار String
اند، تایپچِکر اصلاً راهی برای فهمیدنِ اشتباهِ ما نداره. سعی کنین برای دادههای بیساختار مثل نوشته یا باینری از تایپ مترادف استفاده نکنین. بهترین مورد مصرف برای تایپهای مترادف در مواقعیه که یه چیز سبکتر از newtype
میخواین ولی میخواین تایپ سیگنچر ِتون هم صریحتر باشه.
در آخر هم میرسیم به ضرب با گرامر رکورد:
Prelude> :t RecordProduct
RecordProduct :: a -> b -> RecordProduct a b
Prelude> :t Product
Product :: a -> b -> Product a b
اولین نکته اینکه ساخت ِ مقادیرِ ضرب که با گرامر رکورد تعریف شدن دقیقاً مثل ضربهای بدونِ رکورد ِه. رکورد فقط یه گرامر ِه که به فیلدها اسم مرجع میده:
myRecord :: RecordProduct Integer Float
myRecord = RecordProduct 42 0.00001
برای ساخت ِ مقادیر با یه روش دیگه، میتونیم از اسمِ فیلدهایی که در رکورد تعریف کردیم بهره ببریم. کمک میکنه کُدمون یه کم واضحتر بشه:
myRecord :: RecordProduct Integer Float
myRecord =
RecordProduct { pfirst = 42
, psecond = 0.00001 }
این گرامر برای زمانهایی که اسمهای تخصصی دارین، کاربرد جالبتری داره:
ata OperatingSystem =
GnuPlusLinux
| OpenBSDPlusNevermindJustBSDStill
| Mac
| Windows
deriving (Eq, Show)
ata ProgLang =
Haskell
| Agda
| Idris
| PureScript
deriving (Eq, Show)
data Programmer =
Programmer { os :: OperatingSystem
, lang :: ProgLang }
deriving (Eq, Show)
بعد هم یک مقدار از ضرب ِ رکورددارِ Programmer
میسازیم:
Prelude> :t Programmer
Programmer :: OperatingSystem
-> ProgLang
-> Programmer
nineToFive :: Programmer
nineToFive = Programmer { os = Mac
, lang = Haskell }
-- با گرامر رکورد میشه
-- ترتیب فیلدها رو جابجا کنیم
feelingWizardly :: Programmer
feelingWizardly =
Programmer { lang = Agda
, os = GnuPlusLinux }
تمرین: برنامهنویسها
تابعی بنویسن که همهی مقادیر ممکن با تایپِ Programmer
رو ایجاد کنه. دو لیست شامل همهی اعضای OperatingSystem
و ProgLang
رو براتون تعریف کردیم.
allOperatingSystems :: [OperatingSystem]
allOperatingSystems =
[ GnuPlusLinux
, OpenBSDPlusNevermindJustBSDStill
, Mac
, Windows
]
allLanguages :: [ProgLang]
allLanguages =
[Haskell, Agda, Idris, PureScript]
allProgrammers :: [Programmer]
allProgrammers = undefined
Programmer
ضرب ِ دو تایپه، پس با بیانیهی زیر، تعداد المانهای allProgrammers
رو میشه حساب کرد:
length allOperatingSystems * length allLanguages
این اساسِ رابطهی بین تایپهای ضرب و تعداد سَکَنهی اونهاست.
نوشتن تابعی که اون لیست رو برگردونه راههای زیادی داره، بعضیهاشون هم ممکنه لیستی بِدن که مقادیر تکراری دارن. اگه لیستتون مقادیر تکراری داشت، میتونین با تابع nub
از Data.List
اون مقادیر رو از allProgrammers
حذف کنین. نهایتاً اگه جوابتون (بدون مقادیر تکراری) همون تعداد المانی رو داشت که از ضرب ِ طول ِ اون دو تا لیست پیدا کردین، احتمالاً درست حل کردین. زرنگ باشین و بدون دستی تایپ کردنِ همهی مقادیر به جواب برسین.
تهیهای تصادفی از رکوردها
با همون نوعداده ِ Programmer
، ببینیم اگه یکی از فیلدها رو فراموش کنیم چی میشه:
Prelude> :{
*Main| let partialAf =
*Main| Programmer {os = GnuPlusLinux}
*Main| :}
Field of ‘Programmer’
not initialised: lang
In the expression:
Programmer {os = GnuPlusLinux}
In an equation for ‘partialAf’
partialAf =
Programmer {os = GnuPlusLinux}
-- ...و اگه به این هشدار بیتوجهی کنیم
Prelude> partialAf
Programmer {os = GnuPlusLinux, lang =
*** Exception:
Missing field in
record construction lang
چنین کاری در کُدتون نکنین! یا یکجا کلِ رکورد رو تعریف کنین، یا اصلاً تعریف نکنین. اگه فکر میکنین چنین چیزی لازم دارین، کُدتون نیاز به تغییر داره. اینجا اعمالِ ناقص با دادهسازها کفایت میکنه:
-- همونطوری کار میکنه که اگه
-- .از گرامر رکورد استفاده میکردیم
data ThereYet
There Float Int Bool
deriving (Eq, Show)
nope :: Float -> Int -> Bool -> ThereYet
nope = There
notYet :: Int -> Bool -> ThereYet
notYet = nope 25.5
notQuite :: Bool -> ThereYet
notQuite = notYet 10
yusssss :: ThereYet
yusssss = notQuite False
به پیشرویِ تایپها دقت کنین:
nope :: Float -> Int -> Bool -> ThereYet
notYet :: Int -> Bool -> ThereYet
notQuite :: Bool -> ThereYet
yusssss :: ThereYet
در برنامههاتون از مقادیر استفاده کنین، نه تهی.*
یِتیِ آمریکایی هسکلنویسهای تهی-پخشکن رو برا میانوعده میخوره.
تخریب ِ مقادیر
وقتی فولدها رو گفتیم، از کاتامورفیسم هم صحبت کردیم. گفتیم کاتامورفیسم یعنی تخریب ِ لیستها. این ایده رو میشه عموماً به هر نوعداده ای که حاویِ مقدار باشه اعمال کرد. حالا که ساخت ِ مقادیر رو کامل بررسی کردیم، وقتش رسیده هرچی ساختیم رو نابود کنیم... نه، منظورمون تخریب بود.*
م. لغتِ deconstruct از دو بخشِ construct به معنای ساختن و پیشوندِ de- که برای معکوس کردن به کار میره درست شده، پس صرفاً به معنای "تخریب" نیست.
مثل همیشه با چندتا نوعداده شروع میکنیم:
newtype Name = Name String deriving Show
newtype Acres = Acres Int deriving Show
-- یه جمعه FarmerType
data FarmerType = DairyFarmer
| WheatFarmer
| SoybeanFarmer
deriving Show
-- هم یه ضرب ساده از Farmer
-- هست FarmerType ، وAcres ،Name
data Farmer =
Farmer Name Acres FarmerType
deriving Show
حالا یه تابع خیلی پایهای مینویسیم که داده ِ داخل سازندههامون رو از توشون دَر بیاره:
isDairyFarmer :: Farmer -> Bool
isDairyFarmer (Farmer _ _ DairyFarmer) =
True
isDairyFarmer _ =
False
DairyFarmer
یکی از مقادیرِ تایپِ FarmerType
ِه که داخلِ تایپ ضرب ِ Farmer
بستهبندی شده. ولی این تابعی که تعریف کردیم میتونه اون مقدار رو بکِشه بیرون، روش تطبیقِ الگو کنه، و جوابی که دنبالش هستیم رو بده.
با یه ضربی که از گرامرِ رکورد استفاده میکنه همون تایپ رو تعریف کنیم:
data FarmerRec =
FarmerRec { name :: Name
, acres :: Acres
, farmerType :: FarmerType }
deriving Show
isDairyFarmerRec :: FarmerRec -> Bool
isDairyFarmerRec farmer =
case farmerType farmer of
DairyFarmer -> True
_ -> False
این فقط یه راه دیگه برای باز کردن یا تخریب ِ محتویاتِ یه تایپ ضرب ِه.
تهیهای تصادفی از رکوردها
ما تهیها رو خیلی جدی میگیریم. با تایپهای رکورد، تهی درست کردن خیلی سادهست، ما هم عاجزانه ازتون میخوایم که چنین کاری نکنین. این مثال رو ببینین، لطفاً اینطوری ننویسین:
data Automobile = Null
| Car { make :: String
, model :: String
, year :: Integer }
deriving (Eq, Show)
این واقعاً کار بدیه، به دو دلیل. یکی اون Null
ِ بیخوده. هسکل نوعداده ِ Maybe
به اون خوبی رو داده که اینجور جاها استفاده کنین. دوماً، حالتی رو فرض کنین که از اسمِ یکی از فیلدها با یه مقدارِ Null
استفاده بشه:
Prelude> make Null
*** Exception: No match in
record selector make
-- .نکنین
چطور میشه درستش کرد؟ خوب، اول از همه اگه داخل یه تایپ جمع، یه ضرب دارین که با گرامر رکورد تعریف شده، از تایپ جمع جداش کنین. یعنی بجای اینکه اون ضرب رو با یه دادهسازی که توسطِ تایپِ جمع ایجاد میشه درست کنین، اون رو در یه تایپِ مستقل با نوعساز ِ خودش تعریف کنین:
data Car = Car { make :: String
, model :: String
, year :: Integer }
deriving (Eq, Show)
-- هنوز چیز خوبی نیست، نگهش Null اون
-- داشتیم که منظورمون رو برسونیم
data Automobile = Null
| Automobile Car
deriving (Eq, Show)
حالا اگه یه کار مسخره کنیم، تایپ سیستم گیرمون میندازه:
Prelude> make Null
Couldn't match expected type ‘Car’
with actual type ‘Automobile’
In the first argument of ‘make’,
namely ‘Null’
In the expression: make Null
میخوایم تایپچکر ِ هسکل هرجا اشتباه کردیم بهمون بگه تا قبل از تلنبار شدنِ مشکلات روی هم و بدبختی در زمان اجرا، درستش کنیم. تایپچکر بیشتر از همه به کسانی کمک میکنه که خودشون به خودشون کمک میکنن.