تسمح أنواع 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 محميًا بكلمة مرور آمنة لا تقل عن 8 أحرف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 للصناديق التي كلمة مرورها 8 أحرف أو أكثر.
أما الصناديق Chest التي تستوفي ذلك الشرط، فيجب إضافة شرط securePassword : () الإضافي إلى نوعها الشبح.
لا ينبغي السماح في الزنزانة إلا بأندر الكنوز، فحتى نسخة واحدة تجعل الكنز يبدو رخيصًا.
نفّذ uniqueTreasures التي تأخذ قائمة من Chest وتُرجع قائمة بالعناصر Chest التي كنزها فريد في قائمة الإدخال.
وإذا ظهر كنز مرتين في الإدخال، فلا ينبغي أن يظهر في الناتج.
ويجب ألا تكون لصناديق Chest المُدخَلة شروط محددة، بينما تُضاف إلى الصناديق الناتجة الشرط الإضافي uniqueTreasure : ().
استمتع بأفضل صناديق TreasureChest التي ستجذب المغامرين كما يجذب العسل الذباب.
نفّذ makeTreasureChest التي تأخذ Chest آمنًا وفريدًا في الوقت نفسه وتنشئ TreasureChest.
ولأن TreasureChest نوع معتم، فلن يكون هناك سبيل آخر لإنشاء واحد منها؛ حتى مديرو الزنازين السيئون لن يستطيعوا إفساده.
سجّل في Exercism لتتعلّم وتتقن Elm عبر 28 مفهومًا110 تمارين، وإرشاد بشري حقيقي، وكل ذلك مجانًا.