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

نواصل سلسلة مقالاتنا حول البرمجة الوظيفية في F #. اليوم لدينا موضوع مثير للاهتمام للغاية: تعريف الوظائف. بما في ذلك ، دعونا نتحدث عن وظائف مجهولة المصدر ، وظائف بدون معلمات ، وظائف متكررة ، أدوات دمج ، وأكثر من ذلك بكثير. انظر تحت القط!




تعريف الوظيفة


نحن نعرف بالفعل كيفية إنشاء وظائف منتظمة باستخدام بناء الجملة "let":


let add xy = x + y 

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


وظائف مجهولة المصدر (lambdas)


إذا كنت على دراية باللمدا بلغات أخرى ، فستبدو الفقرات التالية مألوفة. يتم تعريف الوظائف المجهولة (أو "تعبيرات لامدا") على النحو التالي:


 fun parameter1 parameter2 etc -> expression 

بالمقارنة مع lambdas من C # ، هناك اختلافان:


  • يجب أن تبدأ lambdas بالكلمة الأساسية fun ، والتي ليست مطلوبة في C #
  • يستخدم سهم واحد -> ، بدلا من مزدوج => من C #.

تعريف لامدا لوظيفة الجمع:


 let add = fun xy -> x + y 

نفس الوظيفة في الشكل التقليدي:


 let add xy = x + y 

غالبًا ما تستخدم Lambdas في شكل تعبيرات صغيرة أو عندما لا تكون هناك رغبة في تحديد وظيفة منفصلة للتعبير. كما رأيت بالفعل ، عند التعامل مع القوائم ، هذا ليس شائعًا.


 //    let add1 i = i + 1 [1..10] |> List.map add1 //        [1..10] |> List.map (fun i -> i + 1) 

لاحظ أنه يجب استخدام الأقواس حول lambdas.


تستخدم Lambdas أيضًا عند الحاجة إلى وظيفة مختلفة بوضوح. على سبيل المثال ، يمكن إعادة كتابة " adderGenerator " التي ناقشناها سابقًا ، باستخدام lambdas.


 //   let adderGenerator x = (+) x //     let adderGenerator x = fun y -> x + y 

إصدار lambda أطول بقليل ، لكن على الفور يوضح أنه سيتم إرجاع وظيفة وسيطة.


يمكن أن يكون Lambdas متداخلة. مثال آخر على تعريف adderGenerator ، هذه المرة فقط على lambdas.


 let adderGenerator = fun x -> (fun y -> x + y) 

هل أنت واضح أن جميع التعريفات الثلاثة متكافئة؟


 let adderGenerator1 xy = x + y let adderGenerator2 x = fun y -> x + y let adderGenerator3 = fun x -> (fun y -> x + y) 

إذا لم يكن كذلك ، ثم إعادة قراءة الفصل عن الكاري . هذا مهم جدا لفهم!


مطابقة النمط


عند تعريف دالة ، يمكن تمرير المعلمات إليها بشكل صريح ، كما هو موضح في الأمثلة أعلاه ، ولكن من الممكن أيضًا المقارنة مع القالب مباشرةً في قسم المعلمة. بمعنى آخر ، قد يحتوي قسم المعلمات على أنماط (مطابقة الأنماط) ، وليس فقط معرفات!


يوضح المثال التالي استخدام الأنماط في تعريف الوظيفة:


 type Name = {first:string; last:string} //    let bob = {first="bob"; last="smith"} //   //     let f1 name = //   let {first=f; last=l} = name //     printfn "first=%s; last=%s" fl //   let f2 {first=f; last=l} = //        printfn "first=%s; last=%s" fl //  f1 bob f2 bob 

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


 let f3 (x::xs) = //       printfn "first element is=%A" x 

سيقوم المحول البرمجي بتحذير حول المطابقة غير المكتملة (قائمة فارغة ستتسبب في حدوث خطأ في وقت التشغيل عند مدخل هذه الوظيفة).


خطأ شائع: tuples vs. العديد من المعلمات


إذا أتيت من لغة تشبه C ، فإن tuple المستخدم كحجة وحيدة للدالة يمكن أن يشبه بشكل مؤلم وظيفة متعددة المعلمات. لكن هذا ليس الشيء نفسه! كما أشرت سابقًا ، إذا رأيت فاصلة ، فمن المحتمل أن يكون ذلك بمثابة مجموعة. المعلمات مفصولة بمسافات.


مثال الارتباك:


 //      let addTwoParams xy = x + y //      -  let addTuple aTuple = let (x,y) = aTuple x + y //         //        let addConfusingTuple (x,y) = x + y 

  • التعريف الأول ، " addTwoParams " ، يأخذ معلمتين ، مفصولة بمسافة.
  • التعريف الثاني ، " addTuple " ، يأخذ معلمة واحدة. تربط هذه المعلمة "x" و "y" من المجموعة وتجمعها.
  • التعريف الثالث ، " addConfusingTuple " ، يأخذ معلمة واحدة مثل " addTuple " ، ولكن الخدعة هي أن هذا tuple يتم فك حزمه (مطابق للنمط) ويتم addTuple كجزء من تعريف المعلمة باستخدام مطابقة النقش. خلف الكواليس ، يحدث كل شيء تمامًا كما هو الحال في addTuple .

لنلقِ نظرة على التواقيع (انظر دائمًا إليهم إذا لم تكن متأكدًا من شيء ما).


 val addTwoParams : int -> int -> int //   val addTuple : int * int -> int // tuple->int val addConfusingTuple : int * int -> int // tuple->int 

والآن هنا:


 // addTwoParams 1 2 // ok --      addTwoParams (1,2) // error -     // => error FS0001: This expression was expected to have type // int but here has type 'a * 'b 

هنا نرى خطأ في المكالمة الثانية.


أولاً ، يعامل المحول البرمجي (1,2) كـ tuple المعمم من النموذج ('a * 'b) ، والذي يحاول تمريره كمعلمة أولى addTwoParams . بعد ذلك يشكو من أن المعلمة الأولى المتوقعة addTwoParams ليست int ، ولكن جرت محاولة لتمرير tuple.


لجعل tuple ، استخدم فاصلة!


 addTuple (1,2) // ok addConfusingTuple (1,2) // ok let x = (1,2) addTuple x // ok let y = 1,2 //  , //  ! addTuple y // ok addConfusingTuple y // ok 

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


 addConfusingTuple 1 2 // error --          // => error FS0003: This value is not a function and // cannot be applied 

هذه المرة ، قرر المحول البرمجي أنه بمجرد addConfusingTuple ، يجب أن يكون addConfusingTuple . addConfusingTuple 1 " addConfusingTuple 1 " هو تطبيق جزئي ويجب أن يُرجع دالة وسيطة. محاولة استدعاء هذه الوظيفة الوسيطة ذات المعلمة "2" ستؤدي إلى حدوث خطأ ، لأن لا توجد وظيفة وسيطة! نرى نفس الخطأ كما في الفصل الخاص بالكاري ، حيث ناقشنا المشكلات مع الكثير من المعلمات.


لماذا لا تستخدم tuples كمعلمات؟


توضح مناقشة tuples أعلاه طريقة أخرى لتعريف الدوال مع العديد من المعلمات: بدلاً من تمريرها بشكل منفصل ، يمكن تجميع جميع المعلمات في بنية واحدة. في المثال أدناه ، تأخذ الدالة معلمة واحدة - مجموعة من ثلاثة عناصر.


 let f (x,y,z) = x + y * z //  - int * int * int -> int //  f (1,2,3) 

تجدر الإشارة إلى أن التوقيع يختلف عن توقيع دالة بثلاثة معلمات. لا يوجد سوى سهم واحد ومعلمة واحدة وعلامات نجمية تشير إلى المجموعة (int*int*int) .


عندما يكون من الضروري تقديم الحجج مع معلمات منفصلة ، وعندما tuple؟


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

حالة خاصة: .NET مكتبة Tuples والوظائف


عند استدعاء مكتبات .NET ، تكون الفواصل شائعة جدًا!


انهم جميعا يقبلون tuples ، والمكالمات تبدو هي نفسها كما في C #:


 //  System.String.Compare("a","b") //   System.String.Compare "a" "b" 

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


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


 let tuple = ("a","b") System.String.Compare tuple // error System.String.Compare "a","b" // error 

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


 //    let strCompare xy = System.String.Compare(x,y) //    let strCompareWithB = strCompare "B" //      ["A";"B";"C"] |> List.map strCompareWithB 

دليل لاختيار المعلمات الفردية والمجمعة


تؤدي مناقشة tuples إلى موضوع أكثر عمومية: متى يجب أن تكون المعلمات منفصلة ، وعندما يتم تجميعها؟


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


توصيات عامة حول كيفية هيكلة المعلمات عند تصميم الوظائف الخاصة بك.


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

بمعنى آخر ، عند تطوير دالة ، اسأل نفسك ، "هل يمكنني تقديم هذه المعلمة بشكل منفصل؟" إذا كانت الإجابة "لا" ، فيجب تجميع المعلمات.


دعونا نلقي نظرة على بعض الأمثلة:


 //     . //      ,       let add xy = x + y //         //      ,    let locateOnMap (xCoord,yCoord) = //  //      //      -     type CustomerName = {First:string; Last:string} let setCustomerName aCustomerName = //  let setCustomerName first last = //   //     //     //    ,     let setCustomerName myCredentials aName = // 

أخيرًا ، تأكد من أن ترتيب المعلمات سيساعد في التطبيق الجزئي (انظر الدليل هنا ). على سبيل المثال ، لماذا وضعت بيانات myCredentials قبل aName في آخر وظيفة؟


وظائف دون معلمات


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


 let sayHello = printfn "Hello World!" //      

ولكن يمكن إصلاح ذلك عن طريق إضافة معلمة وحدة إلى الوظيفة أو باستخدام لامدا.


 let sayHello() = printfn "Hello World!" //  let sayHello = fun () -> printfn "Hello World!" //  

بعد ذلك ، يجب استدعاء الوظيفة دائمًا باستخدام وسيطة unit :


 //  sayHello() 

ما يحدث غالبًا عند التفاعل مع مكتبات .NET:


 Console.ReadLine() System.Environment.GetCommandLineArgs() System.IO.Directory.GetCurrentDirectory() 

تذكر ، اتصل بهم مع المعلمات unit !


تحديد مشغلين جدد


يمكنك تحديد وظائف باستخدام واحد أو أكثر من أحرف المشغل (انظر الوثائق للحصول على قائمة من الأحرف):


 //  let (.*%) xy = x + y + 1 

يجب عليك استخدام الأقواس حول الحروف لتعريف الوظيفة.


تتطلب العوامل التي تبدأ بـ * مسافة بين قوس و * ، لأن في F # (* يعمل كبداية للتعليق (مثل /*...*/ في C #):


 let ( *+* ) xy = x + y + 1 

بمجرد التعريف ، يمكن استخدام وظيفة جديدة بالطريقة المعتادة إذا كانت ملفوفة بين قوسين:


 let result = (.*%) 2 3 

إذا تم استخدام الوظيفة مع معلمتين ، يمكنك استخدام سجل مشغل infix دون أقواس.


 let result = 2 .*% 3 

يمكنك أيضا تحديد مشغلي البادئة بدءا من ! أو ~ (مع بعض القيود ، راجع الوثائق )


 let (~%%) (s:string) = s.ToCharArray() // let result = %% "hello" 

في F # ، يعد تعريف البيانات عملية شائعة إلى حد ما ، وستصدر العديد من المكتبات عبارات بأسماء مثل >=> و <*> .


نمط نقطة خالية


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


فيما يلي بعض الأمثلة:


 let add xy = x + y //  let add x = (+) x // point free let add1Times2 x = (x + 1) * 2 //  let add1Times2 = (+) 1 >> (*) 2 // point free let sum list = List.reduce (fun sum e -> sum+e) list //  let sum = List.reduce (+) // point free 

هذا النمط له إيجابيات وسلبيات.


تتمثل إحدى الميزات في أن التركيز ينصب على تكوين وظائف الترتيب العالي بدلاً من التركيز على الكائنات ذات المستوى المنخفض. على سبيل المثال ، " (+) 1 >> (*) 2 " هي إضافة صريحة يتبعها الضرب. و " List.reduce (+) " يوضح أن عملية الإضافة مهمة ، بغض النظر عن معلومات القائمة.


يتيح لك النمط غير المجدي التركيز على الخوارزمية الأساسية وتحديد الميزات الشائعة في التعليمات البرمجية. تعتبر وظيفة " reduce " المستخدمة أعلاه مثالًا جيدًا. سيتم مناقشة هذا الموضوع في سلسلة مخططة حول معالجة القائمة.


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


مثل كل شيء في البرمجة ، فإن أفضل توصية هي تفضيل الطريقة التي توفر أكبر قدر من الوضوح.


المجمعات


تسمى " Combinators " بالوظائف التي تعتمد نتيجتها على معلماتها فقط. هذا يعني أنه لا يوجد اعتماد على العالم الخارجي ، وعلى وجه الخصوص ، لا يمكن لأي وظائف أو قيم عالمية أن تؤثر عليهم.


في الممارسة العملية ، هذا يعني أن الوظائف التوافقية محدودة بمجموعة من معالمها بطرق مختلفة.


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


 let (|>) xf = fx //  pipe let (<|) fx = fx //  pipe let (>>) fgx = g (fx) //   let (<<) gfx = g (fx) //   

من ناحية أخرى ، فإن وظائف مثل "printf" ، على الرغم من أنها بدائية ، ليست أدوات دمج لأنها تعتمد على العالم الخارجي (I / O).


الطيور اندماجي


الدمج هي أساس جزء كامل من المنطق (يُطلق عليه بشكل طبيعي "المنطق التوافقي") ، والذي تم اختراعه قبل سنوات عديدة من أجهزة الكمبيوتر ولغات البرمجة. المنطق التوافقي له تأثير كبير على البرمجة الوظيفية.


لمعرفة المزيد عن المجمعات والمنطق الاندماجي ، أوصي بكتاب ريموند سموليان "To Mock a Mockingbird". في ذلك ، يفسر المجمعات الأخرى ويعطي لهم أسماء الطيور بشكل خيالي. فيما يلي بعض الأمثلة عن أدوات الدمج القياسية وأسماء الطيور الخاصة بها:


 let I x = x //  ,  Idiot bird let K xy = x // the Kestrel let M x = x >> x // the Mockingbird let T xy = yx // the Thrush ( !) let Q xyz = y (xz) // the Queer bird ( !) let S xyz = xz (yz) // The Starling //   ... let rec Y fx = f (Y f) x // Y-,  Sage bird 

أسماء الحروف قياسية تمامًا ، لذا يمكنك الرجوع إلى K-combinator لأي شخص مطلع على هذه المصطلحات.


اتضح أن العديد من أنماط البرمجة الشائعة يمكن تمثيلها من خلال هذه المجمعات القياسية. على سبيل المثال ، Kestrel هو نمط منتظم في الواجهة المتقنة حيث تقوم بشيء ما ولكن تقوم بإرجاع الكائن الأصلي. Thrush عبارة عن ماسورة ، Queer عبارة عن تكوين مباشر ، ويقوم Combinator Y بعمل ممتاز لإنشاء وظائف متكررة.


في الواقع ، هناك نظرية معروفة جيدًا مفادها أنه يمكن إنشاء أي وظيفة محسوبة باستخدام مُجمعين أساسيين فقط ، Kestrel و Starling.


المكتبات التوافقية


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


تسمح لك مكتبة مجمعة مصممة جيدًا بالتركيز على الوظائف عالية المستوى وإخفاء "ضجيج" منخفض المستوى. لقد رأينا بالفعل قوتها في العديد من الأمثلة في سلسلة "لماذا تستخدم F #" ، ووحدة List مليئة بهذه الوظائف ، كما أن " fold " و " map " هي أيضًا أدوات دمج إذا كنت تفكر في ذلك.


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


في F # ، تتوفر مكتبات المجمّع للتحليل (FParsec) ، وإنشاء HTML ، واختبار أطر العمل ، إلخ. سوف نناقش ونستخدم أدوات التجميع لاحقًا في السلسلة التالية.


وظائف العودية


غالبًا ما تحتاج الوظيفة إلى الرجوع إلى نفسها من جسمها. مثال كلاسيكي هو وظيفة فيبوناتشي.


 let fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

لسوء الحظ ، لن تتمكن هذه الوظيفة من الترجمة:


 error FS0039: The value or constructor 'fib' is not defined 

يجب أن تخبر المترجم أن هذه دالة عودية باستخدام الكلمة الأساسية rec .


 let rec fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

الدوال العودية وهياكل البيانات شائعة جدًا في البرمجة الوظيفية ، وآمل أن أخصص سلسلة كاملة لهذا الموضوع لاحقًا.


موارد إضافية


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



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


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


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



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


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

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


All Articles