1. الخطوات الأولى
2. الجمع بين الوظائف
3. الاستخدام الجزئي (الكاري)
4. برمجة تعريفية
5. تدوين أساسي
6. الثبات والأشياء
7. الثبات والمصفوفات
8. العدسات
9. الخلاصة
هذا المنشور هو الجزء السادس من سلسلة مقالات حول البرمجة الوظيفية تسمى Ramda Style Thinking.
في الجزء الخامس ، تحدثنا عن وظائف الكتابة بأسلوب تدوين لا طائل منه ، حيث لم يتم تحديد الجدل الرئيسي مع بيانات وظيفتنا بشكل صريح.
في ذلك الوقت ، لم نتمكن من إعادة كتابة جميع وظائفنا بأسلوب بسيط ، لأنه لم يكن لدينا بعض الأدوات اللازمة لذلك. حان الوقت لدراستهم.
قراءة خصائص الكائن
دعونا نلقي نظرة مرة أخرى على مثال تعريف الأشخاص الذين لهم الحق في التصويت ، والذي درسناه في الجزء الخامس :
const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => person.age >= 18 const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen)
كما ترون ، فقد جعلنا isCitizen
و isEligibleToVote
، ولكن لا يمكننا القيام بذلك مع الوظائف الثلاث الأولى.
كما تعلمنا في الجزء الرابع ، يمكننا أن نجعل وظائفنا أكثر وضوحًا من خلال استخدام يساوي و gte . لنبدأ بهذا:
const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18)
لجعل هذه الدوال عديمة الجدوى ، نحتاج إلى طريقة لبناء الوظيفة بحيث نطبق متغير person
في نهاية التعبير. المشكلة هي أننا بحاجة إلى الوصول إلى خصائص person
، والآن نحن نعرف الطريقة الوحيدة للقيام بذلك - وهو أمر حتمي.
سند
لحسن الحظ ، يأتي رمضان مرة أخرى لمساعدتنا. يوفر وظيفة دعم للوصول إلى خصائص الكائنات.
باستخدام prop
، يمكننا إعادة كتابة person.birthCountry
إلى prop('birthCountry', person)
. لنقم بذلك:
const wasBornInCountry = person => equals(prop('birthCountry', person), OUR_COUNTRY) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18)
واو ، الآن يبدو الأمر أسوأ بكثير. ولكن دعونا نواصل إعادة هيكلة لدينا. دعنا نغير ترتيب الحجج التي نمررها على equals
حتى تأتي prop
النهاية. equals
يعمل بنفس الطريقة في الاتجاه المعاكس ، لذلك لن نكسر أي شيء:
const wasBornInCountry = person => equals(OUR_COUNTRY, prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18)
بعد ذلك ، دعنا نستخدم currying ، الخاصية الطبيعية لـ equals
و gte
، من أجل إنشاء وظائف جديدة تنطبق عليها نتيجة استدعاء prop
:
const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(__, 18)(prop('age', person))
لا يزال يبدو الخيار الأسوأ ، ولكن دعنا نستمر. دعونا نستفيد من الكاري مرة أخرى لجميع مكالمات prop
:
const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry')(person)) const wasNaturalized = person => Boolean(prop('naturalizationDate')(person)) const isOver18 = person => gte(__, 18)(prop('age')(person))
مرة أخرى ، بطريقة أو بأخرى ليست كذلك. لكننا الآن نرى نمطًا مألوفًا. جميع وظائفنا لها نفس الصورة f(g(person))
، وكما نعلم من الجزء الثاني ، هذا يعادل compose(f, g)(person)
.
دعنا نطبق هذه الميزة على رمزنا:
const wasBornInCountry = person => compose(equals(OUR_COUNTRY), prop('birthCountry'))(person) const wasNaturalized = person => compose(Boolean, prop('naturalizationDate'))(person) const isOver18 = person => compose(gte(__, 18), prop('age'))(person)
الآن لدينا شيء. تبدو جميع وظائفنا مثل person => f(person)
. ونعلم بالفعل من الجزء الخامس أنه يمكننا جعل هذه الوظائف عديمة الجدوى.
const wasBornInCountry = compose(equals(OUR_COUNTRY), prop('birthCountry')) const wasNaturalized = compose(Boolean, prop('naturalizationDate')) const isOver18 = compose(gte(__, 18), prop('age'))
عندما بدأنا ، لم يكن من الواضح أن أساليبنا قامت بأمرين. لجأوا إلى خاصية الكائن وأعدوا بعض العمليات بقيمته. هذا إعادة الهيكلة إلى أسلوب لا طائل منه جعل هذا صريحًا جدًا.
دعونا نلقي نظرة على بعض الأدوات الأخرى التي يوفرها Ramda للعمل مع الأشياء.
اختر
حيث يقوم prop
بقراءة خاصية واحدة لكائن ما وإرجاع قيمته ، اختر Pick يقرأ العديد من الخصائص من الكائن ويعيد كائنًا جديدًا معهم فقط.
على سبيل المثال ، إذا كنا بحاجة فقط إلى أسماء الأشخاص وسنواتهم ، فيمكننا استخدام pick(['name','age'], person)
.
لديه
إذا أردنا فقط معرفة أن كائننا له خاصية ، دون قراءة قيمته ، فيمكننا استخدام الوظيفة لديه للتحقق من خصائصه ، وكذلك hasIn للتحقق من سلسلة النموذج الأولي: has('name', person)
.
المسار
حيث يقوم prop
خاصية كائن ، يذهب المسار أعمق في الكائنات المتداخلة. على سبيل المثال ، نريد سحب الرمز البريدي من بنية أعمق: path(['address','zipCode'], person)
.
لاحظ أن path
أكثر تسامحا من prop
. سيعود path
undefined
إذا كان أي شيء في المسار (بما في ذلك الوسيطة الأصلية) null
أو undefined
، بينما سيؤدي prop
إلى حدوث خطأ في مثل هذه المواقف.
propOr / pathOr
propOr و pathOr متشابهان مع prop
و path
مع defaultTo
. توفر لك القدرة على تحديد قيمة افتراضية لخاصية أو مسار لا يمكن العثور عليه في الكائن الذي تتم دراسته.
على سبيل المثال ، يمكننا توفير عنصر نائب عندما لا نعرف اسم الشخص: propOr('<Unnamed>, 'name', person)
. لاحظ أنه على عكس prop
، لن يتسبب propOr
حدوث خطأ إذا كان person
null
أو undefined
؛ بدلاً من ذلك ، سيتم إرجاع القيمة الافتراضية.
مفاتيح / قيم
تقوم المفاتيح بإرجاع صفيف يحتوي على كافة أسماء كافة خصائص الكائن المعروفة. ستُرجع القيم قيم هذه الخصائص. يمكن أن تكون هذه الوظائف مفيدة عند دمجها مع وظائف التكرار للمجموعات ، والتي تعلمنا عنها في الجزء الأول .
إضافة وتحديث وحذف الخصائص
الآن لدينا العديد من الأدوات للقراءة من الأشياء بأسلوب إعلاني ، ولكن ماذا عن إجراء التغييرات؟
بما أن الثبات مهم بالنسبة لنا ، فإننا لا نريد تعديل الأشياء مباشرة. بدلاً من ذلك ، نريد إرجاع كائنات جديدة تغيرت بالطريقة التي نريدها.
مرة أخرى ، تقدم لنا رمدة العديد من الفوائد.
assoc / assocPath
عندما نبرمج بأسلوب حتمي ، يمكننا تعيين أو تغيير اسم الشخص من خلال عامل التعيين: person.name = 'New name'
.
في عالمنا الوظيفي غير const updatedPerson = assoc('name', 'newName', person)
للتغيير ، يمكننا استخدام assoc بدلاً من ذلك: const updatedPerson = assoc('name', 'newName', person)
.
assoc
بإرجاع كائن جديد بقيمة خاصية مضافة أو محدثة ، تاركا الكائن الأصلي بدون تغيير.
لدينا أيضا تحت تصرفنا assocPath لتحديث الملكية المرفقة: const updatedPerson = assocPath(['address', 'zipCode'], '97504', person)
.
disoc / dissocPath / omit
ماذا عن حذف الممتلكات؟ حتمًا ، قد نريد أن نقول delete person.age
. في رمضان ، سنستخدم dissoc : `const updatedPerson = dissoc ('age'، person)
dissocPath هو نفسه تقريبًا ، ولكنه يعمل على هياكل كائن أعمق: dissocPath(['address', 'zipCode'], person)
.
ولدينا أيضًا حذف ، والذي يمكنه إزالة العديد من الخصائص في وقت واحد: const updatedPerson = omit(['age', 'birthCountry'], person)
.
يرجى ملاحظة أن pick
omit
متشابهان بعض الشيء ويكمل كل منهما الآخر بشكل جيد للغاية. وهي ملائمة جدًا للإضافة إلى القائمة البيضاء (باستثناء مجموعة معينة فقط من الخصائص باستخدام pick
) والقوائم السوداء (التخلص من خصائص معينة من خلال استخدام omit
).
الآن نحن نعرف ما يكفي للعمل مع الأشياء بأسلوب إعلاني وثابت. دعنا نكتب وظيفة celebrateBirthday
التي تحدّث عمر الشخص في عيد ميلادها.
const nextAge = compose(inc, prop('age')) const celebrateBirthday = person => assoc('age', nextAge(person), person)
هذا نمط شائع جدًا. بدلاً من تحديث الخاصية بقيمة جديدة ، نريد حقًا تغيير القيمة من خلال تطبيق الوظيفة على القيمة القديمة ، كما فعلنا هنا.
لا أعرف طريقة جيدة لكتابة هذا بأقل ازدواجية وبأسلوب أقل صرامة ، مع وجود الأدوات التي تعلمنا عنها سابقًا.
رمدا يحفظنا مرة أخرى بوظيفة التطور . evolve
يقبل كائنًا ويسمح لك بتحديد وظائف التحويل لتلك الخصائص التي نريد تغييرها. دعونا ننكر celebrateBirthday
على استخدام evolve
:
const celebrateBirthday = evolve({ age: inc })
يشير هذا الرمز إلى أننا سنقوم بتحويل الكائن المحدد (الذي لا يتم عرضه بسبب النمط الوحشي) عن طريق إنشاء كائن جديد بنفس الخصائص والقيم ، ولكن سيتم الحصول على خاصية age
عن طريق تطبيق inc
على القيمة الأصلية لخاصية age
.
evolve
يمكن أن تحول العديد من الخصائص في وقت واحد ، وحتى على مستويات متعددة من التعشيش. يمكن أن يكون لتحويل الكائن نفس الصورة التي سيحصل عليها الكائن القابل للتغيير ، evolve
بشكل متكرر بين الهياكل ، باستخدام وظائف التحويل في النموذج المحدد.
لاحظ أن evolve
لا يضيف خصائص جديدة ؛ إذا حددت تحويلًا لخاصية لا تحدث في الكائن الذي تتم معالجته ، evolve
بكل بساطة بتجاهله.
لقد اكتشفت أن evolve
يتحول بسرعة إلى عامل في تطبيقاتي.
دمج الكائنات
تحتاج في بعض الأحيان إلى دمج شيئين معًا. الحالة النموذجية هي عندما يكون لديك وظيفة تأخذ خيارات مسماة ، وترغب في دمجها مع الخيارات الافتراضية. يوفر Ramda دالة دمج لهذا الغرض.
function f(a, b, options = {}) { const defaultOptions = { value: 42, local: true } const finalOptions = merge(defaultOptions, options) }
merge
إرجاع كائن جديد يحتوي على كافة الخصائص والقيم من كلا الكائنات. إذا كان لكلا الكائنين نفس الخاصية ، فسيتم الحصول على قيمة الوسيطة الثانية.
إن وجود هذه القاعدة مع حجة ثانية رابحة يجعل من المفيد استخدام merge
كأداة قائمة بذاتها ، ولكن أقل معنى في حالات النقل. في هذه الحالة ، غالبًا ما تحتاج إلى إعداد سلسلة من التحويلات لكائن ، وأحد هذه التحويلات هو اتحاد بعض قيم الخصائص الجديدة. في هذه الحالة ، سترغب في أن تفوز الحجة الأولى بدلاً من الثانية.
محاولة استخدام merge(newValues)
في خط الأنابيب لن يعطيك ما نود الحصول عليه.
في هذه الحالة ، أقوم عادةً بإنشاء أداة مساعدة خاصة بي تسمى reverseMerge
. يمكن كتابته كـ const reverseMerge = flip(merge)
. flip
المكالمة flip
الوسيطتين الأوليين للدالة التي تنطبق عليها.
يقوم الدمج بدمج السطح. إذا كان للكائنات ، عند دمجها ، خاصية لها قيمة فرعية ، فإن هذه الكائنات الفرعية لا تندمج. لا تمتلك Ramda حاليًا قدرة دمج عميقة (تحتوي المقالة الأصلية التي أترجمها بالفعل على معلومات قديمة حول هذا الموضوع. واليوم ، لدى Ramda وظائف مثل mergeDeepLeft و mergeDeepRight للدمج العميق للكائنات بشكل متكرر ، وطرق أخرى للدمج ).
لاحظ أن merge
يقبل وسيطتين فقط. إذا كنت تريد دمج العديد من الكائنات في كائن واحد ، يمكنك استخدام mergeAll ، الذي يأخذ مصفوفة من الكائنات للجمع.
الخلاصة
اليوم حصلنا على مجموعة رائعة من الأدوات للعمل مع الأشياء بأسلوب إعلاني لا يتغير. يمكننا الآن قراءة الخصائص وإضافتها وتحديثها وحذفها وتحويلها في الكائنات دون تغيير الكائنات الأصلية. ويمكننا القيام بكل هذه الأشياء بأسلوب يسهل الجمع بين الوظائف مع بعضها البعض.
التالي
الآن يمكننا العمل مع الأشياء بأسلوب غير قابل للتغيير ، ولكن ماذا عن المصفوفات؟ "الحصانة والمصفوفات" ستخبرنا ماذا نفعل بها.