۲۴ - ۲معادل تایپی برای توابع رایج

جایی که می‌خوایم این فصل رو باهاش شروع کنیم، ممکنه اول‌ش به نظر عجیب و بی‌ربط بیاد: نیوتایپ‌هایی که یه جورایی معادلِ بعضی تابع‌های پایه‌ای‌اند. دلیلِ اینکه می‌تونیم تایپ‌هایی درست کنیم که مشابهِ اون توابع هستن، اینه که تایپ‌هایی داریم که آرگومان می‌گیرن – یعنی همون نوع‌سازها. دقیق‌تر بخوایم صحبت کنیم، اینجا با تایپ‌هایی کار می‌کنیم که معادلِ ‏‎id‎‏ و ‏‎(.)‎‏ هستند.

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

‏‎Identity‎‏ کسل‌کننده‌ست

این تایپ رو در فصل‌های قبل دیدین، گاهی اوقات یه نوع‌داده بوده، گاهی اوقات هم یه ‏‎newtype‎‏. اینبار متفاوت درست‌ش می‌کنیم: یه نیوتایپ با یه تابع کمکی مشابه چیزی که تو ‏‎Reader‎‏ و ‏‎State‎‏ دیدیم:

newtype Identity a =
  Identity { runIdentity :: a }

دلیل اینکه تو این فصل از ‏‎newtype‎‏ استفاده می‌کنیم اینه که نسخه‌ی موند ترانسفورمر ِ این تایپ، ‏‎IdentityT‎‏ معمولاً با ‏‎newtype‎‏ نوشته میشه. استفاده از پیشوندهای run یا get نشون میده نقشِ تابعِ دستگیره، استخراجِ مقدارِ زیرین از تایپ‌ه. فرقی بین مفهومِ run و get نیست. این تابع‌های دستیگره‌ای رو زیاد می‌بینین، بخصوص با تایپ‌هایی مثلِ ‏‎Identity‎‏ یا حالتِ ترانسفورمر‌ِشون که از تایپِ اصلی استفاده می‌کنن.

نکته‌ای درباره‌ی نیوتایپ‌ها

با اینکه میشه تایپ‌های موند ترانسفورمر رو با کلیدواژه ِ ‏‎data‎‏ هم نوشت، اما معمولاً با ‏‎newtype‎‏ تعریف میشن، و ما هم همین کار رو می‌کنیم. دلیل استفاده از نیوتایپ، فقط جلوگیری از سرباری ِ اضافه‌ست، چون همونطور که به خاطر داریم، در پشت‌پرده نیوتایپ دقیقاً عینِ تایپی که مشمول‌ش هست ارائه میشه. نکته‌ی مهم اینه که موند ترانسفورمرها هیچ وقت تایپ‌های جمع یا ضرب نیستن؛ نقش‌شون فقط پوشوندنِ یک لایه ساختار (موندی) ِ اضافه دورِ یه تایپ‌ه، پس هیچ وقت دلیلی نداره که نتونن نیوتایپ باشن. هسکل‌نویس‌ها بیشتر تمایل دارن از اضافه‌کردن سرباریِ اضافی در زمان اجرا جلوگیری کنن، پس اگه بتونن نیوتایپ‌ِش کنن، اکثراً می‌کنن.

مورد دیگه در رابطه با ‏‎Identity‎‏، شباهتِ کایند‌ِش به تایپِ تابعِ ‏‎id‎‏ ِه، البته با توجه به محدودیت‌های محاسباتی در سطحِ نوعی ِ هسکل، این خیلی مقایسه‌ی دقیقی نیست:

Prelude> :t id
id :: a -> a
Prelude> :k Identity
Identity :: * -> *

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

‏‎Compose‎‏

بالاتر اشاره کردیم که میشه یه تایپ معادلِ تابعِ ترکیب توابع هم درست کنیم.

این زیر، تعریفِ تایپِ ‏‎Compose‎‏ ِه. باید به نظرتون خیلی شبیهِ ترکیب توابع بیاد، اما اینجا ‏‎f‎‏ و ‏‎g‎‏ نوع‌ساز اند، نه تابع‌های سطح جمله‌ای:

newtype Compose f g a =
  Compose { getCompose :: f (g a) }
  deriving (Eq, Show)

پس یه نوع‌سازی داریم که سه آرگومانِ تایپی می‌گیره: ‏‎f‎‏ و ‏‎g‎‏ باید خودشون نوع‌ساز باشن، اما ‏‎a‎‏ یه تایپِ معین ِه (می‌تونین نوع‌سازها رو در کنارِ تابع‌های سطح جمله‌ای، و ثابت‌های تایپی رو در کنارِ مقادیر فرض کنین). اینجا هم مثل بالا یه نگاه به کایند ِ ‏‎Compose‎‏ میندازیم – به کایند ِ آرگومان‌های نوع‌ساز دقت کنین:

Compose :: (* -> *) -> (* -> *) -> * -> *

یادِ چیزی نمیوفتین؟

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

خب در عمل چطور میشه؟ چیزی شبیه این:

Prelude> Compose [Just 1, Nothing]
Compose {getCompose = [Just 1,Nothing]}
Prelude> let xs = [Just (1::Int), Nothing]
Prelude> :t Compose xs
Compose xs :: Compose [] Maybe Int

با فرضِ مقادیرِ بالا، متغیرهای تایپ اینطوری مقید میشن:

Compose [Just (1 :: Int), Nothing]

Compose { getCompose :: f (g    a) }
               Compose [] Maybe Int

f ~ []
g ~ Maybe
a ~ Int

یه ساختاری داریم که یه ساختار ِ دیگه رو پوشونده، و بعد هم یه ثابت تایپی رو (همون ‏‎a‎‏). در نهایت باید به کایند ِ ‏‎*‎‏ برسیم.

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