۱۱ - ۱۳ساخت و تخریب مقادیر

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

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

با تعریفِ چند نوع‌داده شروع می‌کنیم:

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

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