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

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




الاستخدام الجزئي للوظائف


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


//  ""       +  42 let add42 = (+) 42 //    add42 1 add42 3 //       //      [1;2;3] |> List.map add42 //          "" let twoIsLessThan = (<) 2 //   twoIsLessThan 1 twoIsLessThan 3 //      twoIsLessThan [1;2;3] |> List.filter twoIsLessThan //   ""       printfn let printer = printfn "printing param=%i" //      printer    [1;2;3] |> List.iter printer 

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


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


 //    List.map let add1 = (+) 1 let add1ToEach = List.map add1 //   "add1"  List.map //  add1ToEach [1;2;3;4] //    List.filter let filterEvens = List.filter (fun i -> i%2 = 0) //    //  filterEvens [1;2;3;4] 

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


  • نقوم بإنشاء دالة تجمع رقمين ، ولكنها بالإضافة إلى ذلك ، تأخذ وظيفة تسجيل تقوم بتسجيل هذه الأرقام والنتيجة.
  • تأخذ وظيفة التسجيل معلمتين: (سلسلة) "name" و (عام) "value" ، وبالتالي فهي تحتوي على string->'a->unit التوقيع string->'a->unit .
  • ثم نقوم بإنشاء تطبيقات مختلفة لوظيفة التسجيل ، مثل مسجل وحدة التحكم أو المسجل القائم على النوافذ المنبثقة.
  • وأخيرًا ، نقوم بتطبيق الوظيفة الرئيسية جزئيًا لإنشاء وظيفة جديدة ، باستخدام مسجل مغلق.

 //      - let adderWithPluggableLogger logger xy = logger "x" x logger "y" y let result = x + y logger "x+y" result result //  -      let consoleLogger argName argValue = printfn "%s=%A" argName argValue //           let addWithConsoleLogger = adderWithPluggableLogger consoleLogger addWithConsoleLogger 1 2 addWithConsoleLogger 42 99 //  -      let popupLogger argName argValue = let message = sprintf "%s=%A" argName argValue System.Windows.Forms.MessageBox.Show( text=message,caption="Logger") |> ignore //    -       let addWithPopupLogger = adderWithPluggableLogger popupLogger addWithPopupLogger 1 2 addWithPopupLogger 42 99 

يمكن استخدام وظائف المسجل المغلق مثل أي وظائف أخرى. على سبيل المثال ، يمكننا إنشاء تطبيق جزئي لإضافة 42 ، ثم تمريره إلى وظيفة القائمة ، كما فعلنا مع وظيفة add42 البسيطة.


 //         42 let add42WithConsoleLogger = addWithConsoleLogger 42 [1;2;3] |> List.map add42WithConsoleLogger [1;2;3] |> List.map add42 //     

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


تصميم الوظائف الجزئية


من الواضح أن ترتيب المعلمات يمكن أن يؤثر بشكل خطير على راحة الاستخدام الجزئي. على سبيل المثال ، معظم الوظائف في List مثل List.map و List.filter لها شكل مشابه ، وهي:


 List-function [function parameter(s)] [list] 

القائمة هي المعلمة الأخيرة دائمًا. بعض الأمثلة في شكل كامل:


 List.map (fun i -> i+1) [0;1;2;3] List.filter (fun i -> i>1) [0;1;2;3] List.sortBy (fun i -> -i ) [0;1;2;3] 

نفس الأمثلة باستخدام التطبيق الجزئي:


 let eachAdd1 = List.map (fun i -> i+1) eachAdd1 [0;1;2;3] let excludeOneOrLess = List.filter (fun i -> i>1) excludeOneOrLess [0;1;2;3] let sortDesc = List.sortBy (fun i -> -i) sortDesc [0;1;2;3] 

إذا تم تنفيذ وظائف المكتبة بترتيب مختلف من الحجج ، فسيكون التطبيق الجزئي أقل ملاءمة.


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


  1. ابدأ بالخيارات التي من المحتمل أن تكون ثابتة.
  2. كن الأخير لتعيين هياكل البيانات أو المجموعات (أو معلمات متغيرة أخرى)
  3. لفهم أفضل للعمليات مثل الطرح ، من المستحسن مراقبة الترتيب المتوقع

النصيحة الأولى بسيطة. يجب أن تبدأ المعلمات التي من المحتمل أن يتم "إصلاحها" بواسطة التطبيق الجزئي أولاً ، كما هو الحال في الأمثلة مع المسجل أعلاه.


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


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

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


 let compositeOp = List.map (fun i -> i+1) >> List.filter (fun i -> i>5) let result = compositeOp [1..10] 

التفاف جزئي لوظيفة BCL


يمكن الوصول بسهولة إلى وظائف مكتبة الفئة الأساسية (BCL) لـ .NET من F # ، ولكن تم تصميمها دون الاعتماد على اللغات الوظيفية مثل F #. على سبيل المثال ، تتطلب معظم الوظائف معلمة بيانات في البداية ، بينما في F # يجب أن تكون معلمة البيانات هي الأخيرة بشكل عام.


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


 //     .NET  let replace oldStr newStr (s:string) = s.Replace(oldValue=oldStr, newValue=newStr) let startsWith lookFor (s:string) = s.StartsWith(lookFor) 

بعد أن تصبح السلسلة المعلمة الأخيرة ، يمكنك استخدام هذه الوظائف في خطوط الأنابيب ، كالمعتاد:


 let result = "hello" |> replace "h" "j" |> startsWith "j" ["the"; "quick"; "brown"; "fox"] |> List.filter (startsWith "f") 

أو في تكوين الوظائف:


 let compositeOp = replace "h" "j" >> startsWith "j" let result = compositeOp "hello" 

فهم عامل الناقل


بعد أن ترى تطبيقًا جزئيًا في الأعمال التجارية ، يمكنك أن تفهم كيف تعمل وظائف الأنابيب.


يتم تعريف وظيفة الأنابيب على النحو التالي:


 let (|>) xf = fx 

كل ما تفعله هو الجدل قبل الوظيفة ، وليس بعدها.


 let doSomething xyz = x+y+z doSomething 1 2 3 //     

في الحالة التي تحتوي فيها الدالة f على العديد من المعلمات ، وستكون المعلمة الأخيرة للدالة f بمثابة قيمة الإدخال x لخط الأنابيب. في الواقع ، تم بالفعل تطبيق الوظيفة المنقولة f جزئيًا وتتوقع معلمة واحدة فقط - قيمة الإدخال لخط الأنابيب (te x ).


هنا مثال مماثل أعيد كتابته للاستخدام الجزئي.


 let doSomething xy = let intermediateFn z = x+y+z intermediateFn //  intermediateFn let doSomethingPartial = doSomething 1 2 doSomethingPartial 3 //       3 |> doSomethingPartial //    ,        

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


 "12" |> int //   "12"  int 1 |> (+) 2 |> (*) 3 //   

مشغل ناقل عكسي


من وقت لآخر ، قد تواجه عامل خط الأنابيب العكسي "<|".


 let (<|) fx = fx 

يبدو أن هذه الوظيفة لا تفعل شيئًا ، فلماذا توجد؟


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


 printf "%i" 1+2 //  printf "%i" (1+2) //   printf "%i" <| 1+2 //    

يمكنك استخدام خطوط الأنابيب في اتجاهين في آن واحد للحصول على تدوين مزيف.


 let add xy = x + y (1+2) add (3+4) //  1+2 |> add <| 3+4 //   

موارد إضافية


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



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


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


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



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


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

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


All Articles