نوعهای Elm امکان استفاده از نوعهای پارامتری را میدهند که بهطور کلی انعطافپذیری را به یک رابط اضافه میکنند.
برای مثال، نوع Maybe a میتواند مقداری از هر نوعی را نگه دارد.
type Maybe a = Nothing | Just a
توجه کنید که در مثال بالا، پارامتر نوع a در دو طرف علامت تساوی = استفاده شده است.
میگوییم که a به دادهای درون تعریف نوع مقید شده است.
با این حال، در موارد خاصی، پارامتر نوع فقط در سمت چپ معادله ظاهر میشود.
type Distance unit = Distance Float
در تعریف Distance در بالا، unit یک پارامتر نوع آزاد است و به هیچ دادهای در نوع مقید نشده است.
همچنین میتوانیم این نوع را «نوع فانتوم» بنامیم.
زمانی که میخواهیم محدودیتها را در زمان کامپایل اعمال کنیم، بهطور شگفتآوری مفید است. برای مثال، میخواهیم مطمئن شویم که فقط فاصلههایی با یکای یکسان را جمع میکنیم.
-- Distance is an opaque type, since the module does not expose its variants.
-- This means that users may only use the functions meter, foot and add to manipulate distances.
module Distance exposing (Distance, Meter, Foot, meter, foot, add)
-- The Distance type has a phantom type 'unit'.
type Distance unit = Distance Float
-- We define two types that will be used in place
-- of the phantom type 'unit' in our constructor functions.
type Meter = Meter
type Foot = Foot
-- Constructor for Meter
meter : Distance Meter
meter = Distance 1.0
-- Constructor for Foot
foot : Distance Foot
foot = Distance 0.3048
-- The add function cannot take two parameters of different types.
-- So we cannot add meters and feet by mistake.
add : Distance unit -> Distance unit -> Distance unit
add (Distance d1) (Distance d2) = Distance (d1 + d2)
کدی خارج از آن ماژول نمیتواند به درون نوع Distance دسترسی داشته باشد.
کاربران میتوانند دو Distance Meter را جمع کنند، اما نمیتوانند یک Distance Meter را با یک Distance Foot جمع کنند.
import Distance exposing (Distance)
-- Compiles
twoMeters = Distance.add Distance.meter Distance.meter
-- Does not compile
errDist = Distance.add Distance.meter Distance.foot
هیچ محدودیتی وجود ندارد بر اینکه کدام نوعها میتوانند فانتوم باشند، و نوعهای رکورد نیز میتوانند استفاده شوند. وقتی بهعنوان نوع فانتوم استفاده میشوند، رکوردها میتوانند محدودیتهای پیچیدهای را بیان کنند که میتوانند از طریق توابع تغییر کنند.
بیایید یک یکای فاصلهی جدید درون ماژول Distance به نام LegoBlock اضافه کنیم.
بلوکهای فیزیکی، چون اشیای تنظیمشده هستند، همیشه دو ویژگی دارند: فاصلههای غیرکسری و غیرمنفی دارند.
type LegoBlock = LegoBlock
fourStuds : Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
fourStuds = Distance 4.0
nonFractional و nonNegative فیلدهای رکوردی هستند که خارج از یک آرگومان نوع وجود ندارد، پس مناسب است که به آنها نوع () را بدهیم که «نوع واحد» نامیده میشود و هیچ اطلاعاتی فراتر از این واقعیت که آنجاست، نگه نمیدارد.
بهدست آوردن فاصلههای LegoBlock دلخواه البته ممکن است، مثلاً پس از محاسبهی تفاضلها یا نسبتهای فاصله.
negativeStud : Distance { properties | unit: LegoBlock, nonFractional : () }
negativeStud = Distance -1.0
threeFiddyStud : Distance { properties | unit: LegoBlock, nonNegative : () }
threeFiddyStud = Distance 3.50
crazyStud : Distance { properties | unit: LegoBlock }
crazyStud = Distance -13.37
همهی مقادیر بالا معتبرند و کامپایل میشوند، اما ما علاقهی ویژهای به مقادیری مانند fourStuds داریم که هم ویژگی nonFractional و هم nonNegative را دارند، چون بلوکهای فیزیکیای را نمایندگی میکنند که میتوان آنها را با تابع زیر ترکیب کرد:
combineLegoBlocks
: Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
combineLegoBlocks = add
بیایید چند تابع اضافه کنیم تا کاربران بتوانند فاصلههای LegoBlock را ایجاد و پالایش کنند.
newLegoBlock : Float -> Distance { properties | unit: LegoBlock }
newLegoBlock dist = Distance dist
floorDistance : Distance properties -> Distance { properties | nonFractional : () }
floorDistance (Distance dist) = Distance (toFloat (floor dist))
ceilingDistance : Distance properties -> Distance { properties | nonFractional : () }
ceilingDistance (Distance dist) = Distance (toFloat (ceiling dist))
absDistance : Distance properties -> Distance { properties | nonNegative : () }
absDistance (Distance dist) = Distance (abs dist)
توجه کنید که floorDistance، ceilingDistance و absDistance میتوانند یکایی غیر از LegoBlock را هم مدیریت کنند، و بهطور کلی هیچ فرضی دربارهی ویژگیهای ورودی نمیسازند، فقط تضمین میکنند که خروجی ویژگی مشخصی خواهد داشت، یا nonFractional یا nonNegative.
بیایید به چند نتیجه نگاه کنیم.
import Distance exposing (Distance)
-- Compiles
distance1 = combineLegoBlocks fourStuds fourStuds
-- Does not compile
distance2 = combineLegoBlocks fourStuds threeFiddyStud
-- Compiles
distance3 = combineLegoBlocks fourStuds (floorDistance threeFiddyStud)
-- Does not compile
distance4 = combineLegoBlocks fourStuds negativeStud
-- Compiles
distance5 = combineLegoBlocks fourStuds (absDistance negativeStud)
-- Does not compile
distance6 = combineLegoBlocks fourStuds crazyStud
-- Compiles
distance7 = combineLegoBlocks fourStuds (floorDistance (absDistance crazyStud))
-- Compiles
distance8 = combineLegoBlocks fourStuds (ceilingDistance (absDistance crazyStud))
بهطور کلی، floorDistance و ceilingDistance نتایج متفاوتی ارائه میدهند، اما تضمینهای یکسانی دارند.
این قدرت تکنیک نوع فانتوم است: ارائهی گزینههای منعطف به کاربران در حالی که تضمینهای قوی حفظ میشوند.
شما، ارباب شرارت، به کیفیت TreasureChestهایی که در سیاهچالهای شرارتآمیزتان در سراسر جهان یافت میشوند، افتخار بسیاری میکنید.
حفظ این کیفیت آسان نیست؛ مدیران سیاهچالهایتان مدام در گنجسازی خرابکاری میکنند، پس تصمیم میگیرید یک API از Elm ارائه دهید تا آنها را به خواست خودتان وادارید.
دو شرط وجود دارد که هرگز از آنها کوتاه نمیآیید:
۱. هر TreasureChest باید با یک گذرواژهی امن با دستکم ۸ کاراکتر محافظت شود
۲. در یک سیاهچال مشخص، هر TreasureChest باید یک گنج منحصربهفرد را در خود نگه دارد
مدیران سیاهچال شما فهرستی از پیشنهادهای گذرواژه و گنج ارائه میدهند و تنها TreasureChestهای مناسب ساخته میشوند.
با این معیارها، دو راه ممکن برای ساختن صندوقهای امن از یک فهرست پیشنهادها وجود دارد: ۱. صندوقهایی را که گذرواژهی ناامن دارند حذف کنید، سپس صندوقهایی را که گنج تکراری دارند حذف کنید ۲. صندوقهایی را که گنج تکراری دارند حذف کنید، سپس صندوقهایی را که گذرواژهی ناامن دارند حذف کنید
این دو ممکن است نتایج یکسانی به دست ندهند (برای نمونه برای [("strong_password", GoldStatue), ("1234", GoldStatue)])، اما برایتان فرقی نمیکند و میخواهید تصمیم را به مدیران سیاهچال واگذار کنید.
یک API که برای کاربران حق انتخاب باقی میگذارد، اما همچنان ویژگیهای نتیجهی نهایی را تضمین میکند؟ این دقیقاً همان جایی است که تکنیک «نوع فانتوم» به کار میآید!
نوع TreasureChest و همراه آن getTreasure از قبل داده شدهاند، اما باید نوعی برای یک پیشنهاد صندوق بسازید.
نوع Chest را پیادهسازی کنید، makeChest را پیادهسازی کنید و امضای نوع secureChest و uniqueTreasures را درست کنید.
امضای نوع makeChest و makeTreasureChest از قبل بهعنوان بخشی از نیازمندیها داده شده است، آنها را تغییر ندهید.
یک Chest باید همان دادهی TreasureChest را نگه دارد و باید دو آرگومان نوع داشته باشد: یکی برای treasure و یک نوع فانتوم رکوردی برای conditions.
توجه کنید که چون Chest از یک نوع فانتوم استفاده میکند، باید مبهم باشد تا از استفادهی آن در هر جایی بیرون از ماژول TreasureFactory جلوگیری شود.
در این مورد، حتی اصلاً در دسترس قرار نمیگیرد.
امضای نوع secureChest و uniqueTreasures را ویرایش کنید تا محدودیتها را با استفاده از رکوردهای قابلگسترش بهعنوان نوع فانتوم اضافه کنید.
secureChest باید یک Chest بدون شرطهای مشخص بگیرد و یک Maybe Chest برگرداند، با شرط اضافی securePassword : () در رکورد فانتومش.
uniqueTreasures باید یک List Chest بدون شرطهای مشخص بگیرد و یک List Chest برگرداند، با شرط اضافی uniqueTreasure : () که به رکورد فانتوم اضافه میشود.
تستهای Elm به توابع آشکارشده دسترسی دارند، اما به امضای نوعها دسترسی ندارند؛ پس امکانپذیر نیست که تستها بررسی کنند شما از امضاهای درست استفاده میکنید. البته روشنترین نشانهی اینکه امضاهای درست را دارید این است که بتوانید ماژول و تستها را کامپایل و اجرا کنید، اما ما یک قاعدهی تحلیلگر ساختهایم که پس از ارسال راهحلتان امضاهای نوع شما را بررسی میکند.
وقتی صندوقها آماده شدند، باید صندوقهای امن را جدا کنید.
secureChest را پیادهسازی کنید که فقط برای صندوقهایی با گذرواژهی ۸ کاراکتر یا بیشتر، گونهی Just را برمیگرداند.
به نوع فانتوم Chestهایی که این شرط را برآورده میکنند باید شرط اضافی securePassword : () اضافه شود.
فقط کمیابترین گنجها باید در یک سیاهچال مجاز باشند؛ حتی یک نسخهی دیگر هم گنج را ارزان به نظر میرساند.
uniqueTreasures را پیادهسازی کنید که فهرستی از Chest میگیرد و فهرستی از Chestهایی را برمیگرداند که در فهرست ورودی گنجی منحصربهفرد دارند.
اگر گنجی در ورودی دو بار ظاهر شود، نباید در خروجی باشد.
Chestهای ورودی نباید شرطهای مشخص داشته باشند و به Chestهای خروجی باید شرط اضافی uniqueTreasure : () اضافه شود.
از بهترین TreasureChestها لذت ببرید، آنهایی که بیشک ماجراجویان را مثل پروانه به دور شمع جمع میکنند.
makeTreasureChest را پیادهسازی کنید که یک Chest میگیرد که هم امن است و هم منحصربهفرد، و یک TreasureChest میسازد.
چون TreasureChest یک نوع مبهم است، این تنها راه ساختن آن خواهد بود؛ حتی مدیران سیاهچال بیکفایت هم نمیتوانند خرابش کنند.