التفكير الوظيفي. الجزء 10

هل يمكنك أن تتخيل أن هذه هي الدورة العاشرة! على الرغم من أن السرد قد ركز من قبل على أسلوب وظيفي بحت ، إلا أنه من المناسب في بعض الأحيان التبديل إلى نمط وجوه المنحى. وإحدى الميزات الرئيسية للأسلوب الموجه للكائن هي القدرة على إرفاق الوظائف بفصل دراسي والوصول إلى الفصل الدراسي من خلال نقطة للحصول على السلوك المطلوب.






في F # ، هذا ممكن مع ميزة تسمى "ملحقات النوع". أي نوع F # ، وليس مجرد فئة ، يمكن أن يكون له وظائف متصلة.


فيما يلي مثال على ربط دالة بنوع السجل.


module Person = type T = {First:string; Last:string} with // -,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} let person = Person.create "John" "Doe" let fullname = person.FullName 

النقاط الرئيسية التي يجب الانتباه إليها:


  • تشير الكلمة الأساسية with بداية قائمة الأعضاء.
  • تشير الكلمة الأساسية member إلى أن الوظيفة عضو (أي طريقة)
  • الكلمة this هي تسمية الكائن الذي تسمى هذه الطريقة (وتسمى أيضًا "معرف الذات"). هذه الكلمة هي بادئة اسم الوظيفة ، وفي داخلها يمكنك استخدامها للإشارة إلى المثيل الحالي. لا توجد متطلبات للكلمات المستخدمة كمعرف ذاتي ، يكفي أن تكون مستقرة. يمكنك استخدام this ، أو self ، أو أي كلمة أخرى تُستخدم عادةً كمرجع للنفس.

ليست هناك حاجة لإضافة عضو إلى جانب إعلان نوع ، يمكنك دائمًا إضافته لاحقًا في نفس الوحدة:


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //  ,   type T with member this.SortableName = this.Last + ", " + this.First let person = Person.create "John" "Doe" let fullname = person.FullName let sortableName = person.SortableName 

توضح هذه الأمثلة الدعوة إلى "ملحقات جوهرية". يتم تجميعها في نوع وستكون متاحة أينما تم استخدام النوع. كما سيتم عرضها عند استخدام التفكير.


تسمح لك الإضافات الداخلية بتقسيم تعريف النوع إلى ملفات متعددة ، طالما أن جميع المكونات تستخدم مساحة الاسم نفسها وتجميعها في مجموعة واحدة. كما هو الحال مع الفئات الجزئية في C # ، يمكن أن يكون هذا مفيدًا لفصل التعليمات البرمجية المنشأة بخط اليد.


ملحقات اختيارية


البديل هو إضافة عضو إضافي من وحدة نمطية مختلفة تمامًا. يطلق عليهم "ملحقات اختيارية". لا يتم تجميعها داخل الفصل ، وتتطلب وحدة نمطية مختلفة للعمل معهم (هذا السلوك يشبه أساليب الامتداد من C #).


على سبيل المثال ، اسمح بتحديد نوع Person :


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //   ,   type T with member this.SortableName = this.Last + ", " + this.First 

يوضح المثال التالي كيفية إضافة ملحق UppercaseName إليه في وحدة نمطية أخرى:


 //    module PersonExtensions = type Person.T with member this.UppercaseName = this.FullName.ToUpper() 

يمكنك الآن تجربة هذا الامتداد:


 let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

عفوًا ، لقد حصلنا على خطأ. حدث ذلك لأن PersonExtensions ليس في نطاقه. كما في C # ، لاستخدام أي ملحقات ، تحتاج إلى إدخالها في النطاق.


بمجرد القيام بذلك ، ستعمل:


 //    ! open PersonExtensions let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

ملحقات نوع النظام


يمكنك أيضًا توسيع أنواع من مكتبات .NET. ولكن يجب أن يؤخذ في الاعتبار أنه عند توسيع نوع ما ، يجب عليك استخدام اسمه الفعلي ، وليس الاسم المستعار.


على سبيل المثال ، إذا حاولت توسيع int ، فلن ينجح شيء ، لأن int ليس اسمًا صالحًا للنوع:


 type int with member this.IsEven = this % 2 = 0 

بدلاً من ذلك ، استخدم System.Int32 :


 type System.Int32 with member this.IsEven = this % 2 = 0 let i = 20 if i.IsEven then printfn "'%i' is even" i 

أعضاء ثابت


يمكنك إنشاء وظائف ثابتة للأعضاء باستخدام:


  • إضافة static
  • إزالة this العلامة

 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //   static member Create first last = {First=first; Last=last} let person = Person.T.Create "John" "Doe" let fullname = person.FullName 

يمكنك إنشاء أعضاء ثابتة لأنواع النظام:


 type System.Int32 with static member IsOdd x = x % 2 = 1 type System.Double with static member Pi = 3.141 let result = System.Int32.IsOdd 20 let pi = System.Double.Pi 

إرفاق الميزات الموجودة


النمط الشائع جدًا هو إرفاق الوظائف المستقلة الحالية بنوع ما. ويوفر العديد من المزايا:


  • أثناء التطوير ، يمكنك التصريح عن الوظائف المستقلة التي تشير إلى وظائف مستقلة أخرى. سيؤدي ذلك إلى تبسيط عملية التطوير ، نظرًا لأن استنتاج الكتابة يعمل بشكل أفضل بنمط وظيفي مقارنةً بالنمط الموجه للكائن ("نقطة إلى نقطة").
  • ولكن يمكن ربط بعض الوظائف الرئيسية بنوع ما. يتيح ذلك للمستخدمين اختيار أي من الأنماط لاستخدامها - وظيفية أو موجهة للكائن.

مثال على هذا الحل هو وظيفة من مكتبة F # ، والتي تحسب طول القائمة. يمكنك استخدام وظيفة مستقلة من وحدة List أو تسميتها كطريقة مثيل.


 let list = [1..10] //   let len1 = List.length list // -  let len2 = list.Length 

في المثال التالي ، لا يحتوي النوع في البداية على أي أعضاء ، ثم يتم تعريف العديد من الوظائف ، وأخيراً يتم إرفاق دالة fullName بالنوع.


 module Person = // ,     type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last //       type T with member this.FullName = fullName this let person = Person.create "John" "Doe" let fullname = Person.fullName person //  let fullname2 = person.FullName //  

وظيفة fullName لها معلمة واحدة ، person . يتلقى العضو المرفق المعلمة من الارتباط الذاتي.


إضافة وظائف موجودة مع معلمات متعددة


هناك ميزة أخرى لطيفة. إذا كانت الوظيفة المحددة سابقًا تأخذ عدة معلمات ، فعند إرفاقها بنوع ، لن تضطر إلى إدراج كل هذه المعلمات مرة أخرى. يكفي تحديد this المعلمة أولاً.


في المثال أدناه ، hasSameFirstAndLastName الدالة hasSameFirstAndLastName على ثلاثة معلمات. ومع ذلك ، عند إرفاقه يكفي أن نذكر واحد فقط!


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let hasSameFirstAndLastName (person:T) otherFirst otherLast = person.First = otherFirst && person.Last = otherLast //      type T with member this.HasSameFirstAndLastName = hasSameFirstAndLastName this let person = Person.create "John" "Doe" let result1 = Person.hasSameFirstAndLastName person "bob" "smith" //  let result2 = person.HasSameFirstAndLastName "bob" "smith" //  

لماذا تعمل؟ تلميح: التفكير في الكاري والاستخدام الجزئي!


طرق Tuple


عندما يكون لدينا طرق مع أكثر من معلمة واحدة ، تحتاج إلى اتخاذ قرار:


  • يمكننا استخدام النموذج القياسي (المجعد) ، حيث يتم فصل المعلمات بمسافات ، ويدعم التطبيق الجزئي.
  • أو يمكننا تمرير جميع المعلمات في وقت واحد في شكل tuple مفصولة بفواصل.

شكل الكاري أكثر وظيفية ، في حين أن شكل tuple هو أكثر وجوه المنحى.


يستخدم نموذج tuple أيضًا للتفاعل مع F # مع مكتبات .NET القياسية ، لذلك يجب مراعاة هذا النهج بمزيد من التفاصيل.


سيكون موقع الاختبار الخاص بنا نوعًا من Product بطريقتين ، يتم تنفيذ كل واحدة منهما بواسطة إحدى الطرق الموضحة أعلاه. تقوم TupleTotal و TupleTotal بنفس الشيء: فهي تحسب التكلفة الإجمالية للمنتج لكمية وخصم معين.


 type Product = {SKU:string; Price: float} with //   member this.CurriedTotal qty discount = (this.Price * float qty) - discount //   member this.TupleTotal(qty,discount) = (this.Price * float qty) - discount 

رمز الاختبار:


 let product = {SKU="ABC"; Price=2.0} let total1 = product.CurriedTotal 10 1.0 let total2 = product.TupleTotal(10,1.0) 

لا يوجد فرق كبير حتى الآن.


لكننا نعرف أنه يمكن تطبيق النسخة المجففة جزئيًا:


 let totalFor10 = product.CurriedTotal 10 let discounts = [1.0..5.0] let totalForDifferentDiscounts = discounts |> List.map totalFor10 

من ناحية أخرى ، فإن إصدار tuple قادر على شيء لا يمكن تسويته ، وهو:


  • المعلمات المسماة
  • المعلمات الاختيارية
  • الزائد

المعلمات المسماة مع المعلمات في شكل tuple


يدعم الأسلوب tuple المعلمات المسماة:


 let product = {SKU="ABC"; Price=2.0} let total3 = product.TupleTotal(qty=10,discount=1.0) let total4 = product.TupleTotal(discount=1.0, qty=10) 

كما ترون ، يسمح لك هذا بتغيير ترتيب الوسائط من خلال تحديد الأسماء بشكل صريح.


انتباه: إذا كانت بعض المعلمات فقط لها أسماء ، فينبغي دائمًا أن تكون هذه المعلمات في النهاية.


المعلمات الاختيارية مع المعلمات في شكل tuple


بالنسبة للطرق التي تحتوي على معلمات في شكل tuple ، يمكنك تمييز المعلمات على أنها اختيارية باستخدام البادئة في شكل علامة استفهام أمام اسم المعلمة.


  • إذا تم تعيين المعلمة ، فسيتم تمرير Some value إلى الوظيفة
  • وإلا لن يأتي

مثال:


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty match discount with | None -> extPrice | Some discount -> extPrice - discount 

والاختبار:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal2(10) //   let total2 = product.TupleTotal2(10,1.0) 

قد يكون التحقق صراحةً من تحديد None و Some أمرًا شاقًا ، ولكن هناك حلًا أكثر أناقة للتعامل مع المعلمات الاختيارية.


هناك دالة defaultArg التي تأخذ اسم المعلمة كوسيطة الأولى وقيمة افتراضية defaultArg . إذا تم تعيين المعلمة ، فسيتم إرجاع القيمة المقابلة ؛ وإلا ، فإن القيمة الافتراضية.


نفس الكود باستخدام defaulArg :


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty let discount = defaultArg discount 0.0 extPrice - discount 

طريقة الزائد


في C # ، يمكنك إنشاء عدة طرق بنفس الاسم تختلف في توقيعها (على سبيل المثال ، أنواع مختلفة من المعلمات و / أو عددها).


في نموذج وظيفي بحت ، هذا غير منطقي - تعمل الوظيفة مع نوع معين من الوسيطة (المجال) ونوع معين من قيمة الإرجاع (النطاق). لا يمكن أن تتفاعل نفس الوظيفة مع النطاق والنطاق الآخرين.


ومع ذلك ، فإن F # يدعم التحميل الزائد للطريقة ، ولكن فقط للطرق (التي يتم إرفاقها بالأنواع) وفقط تلك التي تتم كتابتها بنمط tuple.


هنا مثال مع صيغة أخرى لأسلوب TupleTotal !


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal3(qty) = printfn "using non-discount method" this.Price * float qty //   member this.TupleTotal3(qty, discount) = printfn "using discount method" (this.Price * float qty) - discount 

كقاعدة عامة ، يقسم المترجم F # أن هناك طريقتين بنفس الاسم ، ولكن في هذه الحالة يكون هذا مقبولًا ، يتم الإعلان عنها بترميز tuple وتواقيعها مختلفة. (لتوضيح الطريقة التي يتم استدعاءها ، أضفت رسائل صغيرة لتصحيح الأخطاء)


مثال للاستخدام:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal3(10) //   let total2 = product.TupleTotal3(10,1.0) 

مهلا! ليس بهذه السرعة ... عيوب استخدام الأساليب


أنت قادم من عالم موجه للكائنات ، يمكنك إغراء استخدام الأساليب في كل مكان ، لأنه شيء مألوف. ولكن يجب أن تكون حذرا ، لأنه لديهم عدد من العيوب الخطيرة:


  • الأساليب لا تعمل بشكل جيد مع استنتاج النوع
  • الأساليب لا تعمل بشكل جيد مع وظائف النظام العالي

في الواقع ، من خلال استغلال الأساليب ، يمكنك تفويت أقوى جوانب البرمجة المفيدة في F #.


دعونا نرى ما أعنيه.


تتفاعل الطرق بشكل سيء مع الاستدلال النوعي


دعنا نعود إلى المثال مع Person ، حيث تم تنفيذ نفس المنطق في وظيفة مستقلة وبأسلوب:


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last // - type T with member this.FullName = fullName this 

الآن دعنا نرى كيف تعمل كتابة الاستدلال مع كل طريقة من الطرق. افترض أنني أريد طباعة الاسم الكامل للشخص ، ثم printFullName وظيفة printFullName ، والتي تأخذ person كمعلمة.


رمز باستخدام وظيفة مستقلة عن الوحدة النمطية:


 open Person //    let printFullName person = printfn "Name is %s" (fullName person) //    // val printFullName : Person.T -> unit 

إنه يجمع دون مشاكل ، ونوع الاستدلال يعرّف المعلمة Person بشكل صحيح.


جرب الآن الإصدار من خلال النقطة:


 open Person //    " " let printFullName2 person = printfn "Name is %s" (person.FullName) 

هذا الكود لا يجمع على الإطلاق ، لأنه لا يحتوي type inference على معلومات كافية لتحديد نوع المعلمة. يمكن تطبيق أي كائن .FullName - هذا لا يكفي للإخراج.


نعم ، يمكننا التعليق على وظيفة بنوع المعلمة ، ولكن لهذا السبب ، يتم فقدان النقطة الكاملة لاستنتاج الكتابة التلقائية.


أساليب يذهب سيئة مع وظائف الترتيب العالي


مشكلة مماثلة تنشأ في وظائف النظام العالي. على سبيل المثال ، هناك قائمة بالأشخاص ، ونحن بحاجة إلى الحصول على قائمة بأسمائهم الكاملة.


في حالة وجود وظيفة مستقلة ، يكون الحل تافهًا:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //     list |> List.map fullName 

في حالة وجود طريقة كائن ، يجب عليك إنشاء لامدا خاص في كل مكان:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //    list |> List.map (fun p -> p.FullName) 

ولكن هذا لا يزال مثال بسيط إلى حد ما. طرق الكائنات قابلة تمامًا للتكوين ، وغير مريحة في خط الأنابيب ، إلخ.


لذلك ، إذا كنت جديدًا في البرمجة الوظيفية ، فأنا أحثك ​​على ذلك: إذا استطعت ، لا تستخدم الطرق ، خاصة في عملية التعلم. ستكون عكازًا لن تسمح لك باستخراج أقصى فائدة من البرمجة الوظيفية.


موارد إضافية


هناك العديد من البرامج التعليمية لـ F # ، بما في ذلك المواد لأولئك الذين يأتون مع تجربة C # أو Java. قد تكون الروابط التالية مفيدة كلما تعمقت في F #:



موصوفة أيضًا عدة طرق أخرى لبدء التعلم F # .


أخيرًا ، يعتبر مجتمع F # صديقًا للمبتدئين جدًا. هناك دردشة نشطة للغاية في Slack ، بدعم من F # Software Foundation ، مع غرف مبتدئين يمكنك الانضمام إليها بحرية . نحن نوصي بشدة أن تفعل هذا!


لا تنسَ زيارة موقع مجتمع الناطقين بالروسية F # ! إذا كانت لديك أي أسئلة حول تعلم اللغة ، فسيسعدنا مناقشتها في غرف الدردشة:



حول مؤلفي الترجمة


ترجم بواسطة kleidemos
تم إجراء تغييرات في التحرير والتحرير من خلال جهود مجتمع الناطقين باللغة الروسية من مطوري F # . نشكر أيضًا @ schvepsss و @ shwars على إعداد هذه المقالة للنشر.

Source: https://habr.com/ru/post/ar433410/


All Articles