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

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




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


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


 val functionName : domain -> range 

بعض الأمثلة على الوظائف:


 let intToString x = sprintf "x is %i" x //  int  string let stringToInt x = System.Int32.Parse(x) 

إذا قمت بتنفيذ هذا الرمز في نافذة تفاعلية ، يمكنك رؤية التوقيعات التالية:


 val intToString : int -> string val stringToInt : string -> int 

يقصدون:


  • يحتوي intToString على مجال من النوع int ، والذي يعيّن نطاق string النوع.
  • يحتوي stringToInt على نطاق من نوع string ، والذي يتم stringToInt إلى نطاق من النوع int .

أنواع بدائية


هناك أنواع بدائية متوقعة: السلسلة ، int ، float ، bool ، char ، byte ، وما إلى ذلك ، بالإضافة إلى العديد من المشتقات الأخرى لنظام .NET type.


زوجان آخران من الأمثلة على الوظائف ذات الأنواع البدائية:


 let intToFloat x = float x // "float" -  int  float let intToBool x = (x = 2) // true  x  2 let stringToString x = x + " world" 

وتوقيعاتهم:


 val intToFloat : int -> float val intToBool : int -> bool val stringToString : string -> string 

اكتب تعليق توضيحي


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


 let stringLength x = x.Length => error FS0072: Lookup on object of indeterminate type 

لا يعرف المترجم نوع الوسيطة "x" ، ولهذا السبب ، لا يعرف ما إذا كان "الطول" طريقة صالحة. في معظم الحالات ، يمكن إصلاح هذا بتمرير "نوع التعليق" إلى المترجم F #. ثم سيعرف أي نوع لاستخدامه. في الإصدار الثابت ، نشير إلى أن النوع "x" هو سلسلة.


 let stringLength (x:string) = x.Length 

الأقواس حول معلمة x:string مهمة. إذا تم تخطيها ، سيقرر المترجم أن السلسلة هي القيمة المرجعة! بمعنى ، يتم استخدام النقطتين المفتوحتين للإشارة إلى نوع القيمة المرتجعة ، كما هو موضح في المثال التالي.


 let stringLengthAsInt (x:string) :int = x.Length 

نشير إلى أن المعلمة x هي سلسلة ، وأن القيمة المرجعة هي عدد صحيح.


أنواع الوظائف كمعلمات


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


ضع في اعتبارك الدالة evalWith5ThenAdd2 ، التي تأخذ دالة كمعلمة ، ثم تحسب هذه الدالة من 5 وتضيف 2 إلى النتيجة:


 let evalWith5ThenAdd2 fn = fn 5 + 2 //   ,   fn(5) + 2 

يبدو توقيع هذه الوظيفة كما يلي:


 val evalWith5ThenAdd2 : (int -> int) -> int 

يمكنك أن ترى أن المجال (int->int) والنطاق int . ماذا يعني هذا؟ هذا يعني أن معلمة الإدخال ليست قيمة بسيطة ، ولكنها دالة من العديد من الوظائف من int إلى int . قيمة الخرج ليست دالة ، ولكنها مجرد int .


لنجرب:


 let add1 x = x + 1 //  -  (int -> int) evalWith5ThenAdd2 add1 //   

واحصل على:


 val add1 : int -> int val it : int = 8 

" add1 " هي وظيفة add1 int إلى int ، كما نرى من التوقيع. إنها معلمة صالحة لـ evalWith5ThenAdd2 ، والنتيجة هي 8.


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


حالة أخرى:


 let times3 x = x * 3 // -  (int -> int) evalWith5ThenAdd2 times3 //   

يعطي:


 val times3 : int -> int val it : int = 17 

" times3 " هي أيضًا وظيفة times3 int إلى int ، كما يمكن رؤيته من التوقيع. وهي أيضًا معلمة صالحة لـ evalWith5ThenAdd2 . نتيجة الحسابات هي 17.


يرجى ملاحظة أن بيانات الإدخال حساسة للنوع. إذا كانت الوظيفة التي تم تمريرها تستخدم float ، وليس int ، فلن يعمل شيء. على سبيل المثال ، إذا كان لدينا:


 let times3float x = x * 3.0 // -  (float->float) evalWith5ThenAdd2 times3float 

المحول البرمجي ، عند محاولة الترجمة ، سيعيد خطأ:


 error FS0001: Type mismatch. Expecting a int -> int but given a float -> float 

تقرير أن وظيفة الإدخال يجب أن تكون دالة من النوع int->int .


وظائف الإخراج


يمكن أن تكون دالات القيمة نتيجة الدالات. على سبيل المثال ، ستنشئ الوظيفة التالية دالة "adder" تضيف قيمة إدخال.


 let adderGenerator numberToAdd = (+) numberToAdd 

توقيعها:


 val adderGenerator : int -> (int -> int) 

يعني أن المولد يأخذ int ويخلق وظيفة ("adder") تعمل على تعيين ints إلى ints . دعنا نرى كيف يعمل:


 let add1 = adderGenerator 1 let add2 = adderGenerator 2 

يتم إنشاء دالتين adder. الأول ينشئ وظيفة تضيف 1 إلى الإدخال ، ويضيف الثاني 2. لاحظ أن التوقيعات هي بالضبط ما كنا نتوقعه.


 val add1 : (int -> int) val add2 : (int -> int) 

الآن يمكنك استخدام الوظائف التي تم إنشاؤها كالمعتاد ، فهي لا تختلف عن الوظائف المحددة بشكل صريح:


 add1 5 // val it : int = 6 add2 5 // val it : int = 7 

استخدام التعليقات التوضيحية على النوع لتقييد أنواع الوظائف


في المثال الأول ، نظرنا إلى دالة:


 let evalWith5ThenAdd2 fn = fn 5 +2 > val evalWith5ThenAdd2 : (int -> int) -> int 

في هذا المثال ، يمكن أن يستنتج F # أن " fn " تحول int إلى int ، لذا فإن توقيعها سيكون int->int .


ولكن ما هو توقيع "fn" في الحالة التالية؟


 let evalWith5 fn = fn 5 

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


 let evalWith5AsInt (fn:int->int) = fn 5 let evalWith5AsFloat (fn:int->float) = fn 5 

بالإضافة إلى ذلك ، يمكنك تحديد نوع الإرجاع.


 let evalWith5AsString fn :string = fn 5 

لأن ترجع الدالة الرئيسية string ، وتضطر الدالة " fn " أيضًا إلى إرجاع string . وبالتالي ، ليس من الضروري تحديد النوع " fn " بشكل صريح.


اكتب "وحدة"


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


 let printInt x = printf "x is %i" x //    

ما هو توقيعها؟


 val printInt : int -> unit 

ما هي " unit


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



لذا ، في F # ، ترجع الدالات مثل هذه نوعًا خاصًا من النتائج يسمى " unit ". يحتوي على قيمة واحدة فقط ، يُشار إليها بـ " () ". قد تعتقد أن unit و () هما شيئًا مثل "void" و "null" من C # ، على التوالي. ولكن على عكسهم ، unit هي النوع الحقيقي ، و () القيمة الحقيقية. للتحقق من ذلك ، قم فقط بما يلي:


 let whatIsThis = () 

سيتم استلام التوقيع التالي:


 val whatIsThis : unit = () 

مما يشير إلى أن التسمية " whatIsThis " من نوع unit وترتبط بقيمة () .


الآن ، بالعودة إلى توقيع " printInt " ، يمكننا أن نفهم معنى هذا الإدخال:


 val printInt : int -> unit 

يقول هذا التوقيع أن printInt لديها مجال int ، والذي يترجم إلى شيء لا يهمنا.


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


الآن بعد أن فهمنا unit ، هل يمكننا التنبؤ بمظهرها في سياق مختلف؟ على سبيل المثال ، حاول إنشاء وظيفة قابلة لإعادة الاستخدام "hello world". نظرًا لعدم وجود إدخال أو إخراج ، يمكننا توقع unit -> unit التوقيع unit -> unit . دعونا نرى:


 let printHello = printf "hello world" //    

النتيجة:


 hello world val printHello : unit = () 

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


 val aName: type = constant 

في هذا المثال ، نرى أن printHello الواقع قيمة بسيطة () . هذه ليست وظيفة يمكننا الاتصال بها لاحقًا.


ما الفرق بين printInt و printHello ؟ في حالة printInt لا يمكن تحديد القيمة حتى نعرف قيمة المعلمة x ، لذلك كان التعريف دالة. في حالة printHello لا توجد معلمات ، لذلك يمكن تحديد الجانب الأيمن في المكان. وكان يساوي () مع تأثير جانبي في شكل إخراج إلى وحدة التحكم.


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


 let printHelloFn () = printf "hello world" //    

الآن توقيعها يساوي:


 val printHelloFn : unit -> unit 

ولكي نسميها ، يجب علينا تمرير () كمعلمة:


 printHelloFn () 

تقوية أنواع الوحدات مع وظيفة التجاهل


في بعض الحالات ، يحتاج المترجم إلى نوع unit ويشكو. على سبيل المثال ، ستؤدي كلتا الحالتين التاليتين إلى خطأ في المحول البرمجي:


 do 1+1 // => FS0020: This expression should have type 'unit' let something = 2+2 // => FS0020: This expression should have type 'unit' "hello" 

للمساعدة في هذه المواقف ، هناك وظيفة ignore خاصة تأخذ أي شيء وتعيد unit . يمكن أن تكون النسخة الصحيحة من هذا الرمز هي:


 do (1+1 |> ignore) // ok let something = 2+2 |> ignore // ok "hello" 

أنواع عامة


في معظم الحالات ، إذا كان نوع معلمة الدالة يمكن أن يكون أي نوع ، نحتاج إلى قول شيء عنه. يستخدم F # مولدات NET. لمثل هذه المواقف.


على سبيل المثال ، تحول الوظيفة التالية معلمة إلى سلسلة بإضافة بعض النص:


 let onAStick x = x.ToString() + " on a stick" 

بغض النظر عن نوع المعلمة ، يمكن لجميع الكائنات القيام به في ToString() .


التوقيع:


 val onAStick : 'a -> string 

ما نوع 'a ؟ في F # ، هي طريقة للإشارة إلى نوع عام غير معروف في وقت الترجمة. الفاصلة العليا قبل "a" تعني أن النوع عام. يعادل هذا التوقيع في C #:


 string onAStick<a>(); //   string OnAStick<TObject>(); // F#-   'a    // C#'-   "TObject"   

يجب أن يكون مفهوما أن هذه الوظيفة F # لا تزال لديها كتابة قوية حتى مع الأنواع العامة. لا يقبل معلمة من نوع Object . الكتابة القوية جيدة لأنها تسمح لك بالحفاظ على أمان النوع الخاص بها عند إنشاء الوظائف.


يتم استخدام نفس الوظيفة في int و float و string .


 onAStick 22 onAStick 3.14159 onAStick "hello" 

إذا كانت هناك معلمتان معممتان ، فسيعطيهما المترجم اسمين مختلفين: 'a للأول ، 'b للثاني ، إلخ. على سبيل المثال:


 let concatString xy = x.ToString() + y.ToString() 

سيكون هناك نوعان عامان في هذا التوقيع: 'a و 'b :


 val concatString : 'a -> 'b -> string 

من ناحية أخرى ، يتعرف المترجم عند الحاجة إلى نوع عام واحد فقط. في المثال التالي ، يجب أن تكون x و y من نفس النوع:


 let isEqual xy = (x=y) 

لذا ، فإن توقيع الوظيفة له نفس النوع العام لكل من المعلمتين:


 val isEqual : 'a -> 'a -> bool 

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


أنواع أخرى


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


  • Tuples هذا زوج ، ثلاثي ، إلخ. ، مؤلف من أنواع أخرى. على سبيل المثال ، ("hello", 1) هي مجموعة tuple تعتمد على string و int . الفاصلة هي السمة المميزة للصفوف ؛ إذا شوهدت فاصلة في مكان ما في F # ، فهذا يكاد يكون مضمونًا ليكون جزءًا من الصف
    في توقيعات الوظيفة ، تتم كتابة الصفوف على أنها "منتجات" من النوعين المعنيين. في هذه الحالة ، سيكون الصف من النوع:

 string * int // ("hello", 1) 

  • مجموعات . وأكثرها شيوعًا هي القائمة (القائمة) ، والتالية (التسلسل) والمصفوفة. القوائم والمصفوفات ثابتة في الحجم ، في حين أن التسلسلات قد تكون لانهائية (خلف الكواليس ، التسلسلات هي نفسها IEnumrable ). في التوقيعات الوظيفية ، لديهم كلماتهم الرئيسية: " list " ، " seq " و " [] " للمصفوفات.

 int list // List type  [1;2;3] string list // List type  ["a";"b";"c"] seq<int> // Seq type  seq{1..10} int [] // Array type  [|1;2;3|] 

  • خيار (نوع اختياري) . هذا غلاف بسيط فوق الأشياء التي قد تكون مفقودة. هناك خياران: Some (عند وجود القيمة) ولا None (عندما None تكون القيمة). في التوقيعات الوظيفية ، لديهم الكلمة الرئيسية الخاصة بهم " option ":

 int option // Some 1 

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

اختبر فهمك للأنواع


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


 let testA = float 2 let testB x = float 2 let testC x = float 2 + x let testD x = x.ToString().Length let testE (x:float) = x.ToString().Length let testF x = printfn "%s" x let testG x = printfn "%f" x let testH = 2 * 2 |> ignore let testI x = 2 * 2 |> ignore let testJ (x:int) = 2 * 2 |> ignore let testK = "hello" let testL() = "hello" let testM x = x=x let testN x = x 1 // :     x? let testO x:string = x 1 // :    :string ? 

موارد إضافية


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



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


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


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



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


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

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


All Articles