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

الجواب بسيط للغاية: تتم إعادة كتابة دالة بعدة معلمات كسلسلة من الوظائف الجديدة ، كل منها يأخذ معلمة واحدة فقط. يقوم المترجم بتنفيذ هذه العملية تلقائيًا ، ويطلق عليه " currying " ، تكريماً لهيسكيل كاري ، عالم الرياضيات الذي أثر بشكل كبير على تطوير البرمجة الوظيفية.
لمعرفة كيفية عمل الكاري عمليًا ، دعنا نستخدم مثال رمز بسيطًا يطبع رقمين:
// let printTwoParameters xy = printfn "x=%iy=%i" xy
في الواقع ، يقوم المترجم بإعادة كتابته في الشكل التالي تقريبًا:
// let printTwoParameters x = // let subFunction y = printfn "x=%iy=%i" xy // , subFunction //
خذ بعين الاعتبار هذه العملية بمزيد من التفاصيل:
- تم الإعلان عن وظيفة باسم "
printTwoParameters
" ، ولكن مع قبول معلمة واحدة فقط: "x". - يتم إنشاء دالة محلية بداخلها ، والتي تأخذ أيضًا معلمة واحدة فقط: "y". لاحظ أن الدالة المحلية تستخدم المعلمة "x" ، ولكن لا يتم تمرير x إليها كوسيطة. "x" في هذا النطاق بحيث يمكن للدالة المتداخلة رؤيتها واستخدامها دون الحاجة إلى تمريرها.
- وأخيرًا ، يتم إرجاع الوظيفة المحلية التي تم إنشاؤها حديثًا.
- يتم تطبيق الدالة التي تم إرجاعها على الوسيطة "y". يتم إغلاق المعلمة "x" بحيث تحتاج الدالة التي تم إرجاعها إلى المعلمة "y" فقط لإكمال منطقها.
من خلال إعادة كتابة الوظائف بهذه الطريقة ، يضمن المترجم أن كل وظيفة تقبل معلمة واحدة فقط ، كما هو مطلوب. وبالتالي ، باستخدام " printTwoParameters
" ، قد تعتقد أن هذه دالة ذات معلمتين ، ولكن في الواقع يتم استخدام وظيفة ذات معلمة واحدة فقط. يمكنك التحقق من ذلك بتمرير وسيطة واحدة فقط بدلاً من اثنتين:
// printTwoParameters 1 // val it : (int -> unit) = <fun:printTwoParameters@286-3>
إذا قمنا بحسابها باستخدام وسيطة واحدة ، فلن نحصل على خطأ - سيتم إرجاع الوظيفة.
إذن ، هذا ما يحدث بالفعل عندما يتم استدعاء printTwoParameters
مع printTwoParameters
:
- يتم
printTwoParameters
مع الوسيطة الأولى (x) printTwoParameters
بإرجاع دالة جديدة يتم فيها إغلاق "x".- ثم يتم استدعاء دالة جديدة مع الوسيطة الثانية (ص)
فيما يلي مثال على خطوة بخطوة وإصدار عادي:
// let x = 6 let y = 99 let intermediateFn = printTwoParameters x // - // x let result = intermediateFn y // let result = (printTwoParameters x) y // let result = printTwoParameters xy
هنا مثال آخر:
// let addTwoParameters xy = x + y // let addTwoParameters x = // ! let subFunction y = x + y // subFunction // // let x = 6 let y = 99 let intermediateFn = addTwoParameters x // - // x let result = intermediateFn y // let result = addTwoParameters xy
مرة أخرى ، "الوظيفة ذات المعلمتين" هي في الواقع دالة ذات معلمة واحدة ، والتي تُرجع دالة وسيطة.
ولكن انتظر ، ماذا عن عامل التشغيل +
؟ هل هذه عملية ثنائية يجب أن تأخذ معلمتين؟ لا ، هي أيضًا منظمة ، مثل الوظائف الأخرى. هذه دالة تسمى " +
" تأخذ معلمة واحدة وترجع دالة وسيطة جديدة ، تمامًا مثل addTwoParameters
أعلاه.
عندما نكتب التعبير x+y
، فإن المترجم يعيد ترتيب الشفرة بطريقة تحول infix إلى (+) xy
، وهي دالة باسم +
تأخذ معلمتين. لاحظ أن الدالة "+" تحتاج إلى أقواس للإشارة إلى أنها تستخدم كوظيفة عادية ، وليس كعامل infix.
أخيرًا ، يتم التعامل مع دالة ذات معلمتين تسمى +
، مثل أي وظيفة أخرى ذات معلمتين.
// let x = 6 let y = 99 let intermediateFn = (+) x // "" "" let result = intermediateFn y // let result = (+) xy // let result = x + y
ونعم ، هذا يعمل مع جميع العوامل الأخرى والوظائف المضمنة مثل printf
.
// let result = 3 * 5 // - let intermediateFn = (*) 3 // "" 3 let result = intermediateFn 5 // printfn let result = printfn "x=%iy=%i" 3 5 // printfn - let intermediateFn = printfn "x=%iy=%i" 3 // "3" let result = intermediateFn 5
توقيعات وظيفة الكاري
الآن بعد أن عرفنا كيف تعمل الوظائف المتقنة ، من المثير للاهتمام معرفة كيف ستبدو توقيعاتهم.
بالعودة إلى المثال الأول ، " printTwoParameter
" ، رأينا أن الدالة تأخذ وسيطة واحدة وتقوم بإرجاع دالة وسيطة. أخذت الدالة المتوسطة أيضًا حجة واحدة ولم تُرجع شيئًا (أي unit
). لذلك كانت الوظيفة الوسيطة من نوع int->unit
. بمعنى آخر ، المجال printTwoParameters
هو int
، والنطاق هو int->unit
. عند وضع كل ذلك معًا ، سنرى التوقيع النهائي:
val printTwoParameters : int -> (int -> unit)
إذا حسبت تنفيذًا واضحًا صريحًا ، يمكنك رؤية الأقواس في التوقيع ، ولكن إذا حسبت تنفيذًا عاديًا ، ومضمونًا بشكل ضمني ، فلن تكون هناك أقواس:
val printTwoParameters : int -> int -> unit
الأقواس اختيارية. ولكن يمكن تمثيلها في العقل لتبسيط تصور التواقيع الوظيفية.
وما الفرق بين دالة ترجع دالة وسيطة ودالة عادية ذات معلمتين؟
فيما يلي دالة بمعلمة واحدة تقوم بإرجاع دالة أخرى:
let add1Param x = (+) x // signature is = int -> (int -> int)
وهنا دالة ذات معلمتين تقوم بإرجاع قيمة بسيطة:
let add2Params xy = (+) xy // signature is = int -> int -> int
تختلف توقيعاتهم اختلافًا طفيفًا ، ولكن بالمعنى العملي لا يوجد فرق كبير بينهم ، باستثناء حقيقة أن الوظيفة الثانية تتم تلقائيًا.
وظائف بأكثر من معلمتين
كيف يعمل الكاري للوظائف ذات أكثر من معلمتين؟ بنفس الطريقة: لكل معلمة ، باستثناء الأخيرة ، تُرجع الدالة دالة وسيطة تغلق المعلمة السابقة.
تأمل في هذا المثال الصعب. لقد أعلنت صراحة أنواع المعلمات ، ولكن الوظيفة لا تفعل شيئًا.
let multiParamFn (p1:int)(p2:bool)(p3:string)(p4:float)= () // let intermediateFn1 = multiParamFn 42 // multoParamFn int (bool -> string -> float -> unit) // intermediateFn1 bool // (string -> float -> unit) let intermediateFn2 = intermediateFn1 false // intermediateFn2 string // (float -> unit) let intermediateFn3 = intermediateFn2 "hello" // intermediateFn3 float // (unit) let finalResult = intermediateFn3 3.141
توقيع الوظيفة بكاملها:
val multiParamFn : int -> bool -> string -> float -> unit
وتوقيعات الوظائف المتوسطة:
val intermediateFn1 : (bool -> string -> float -> unit) val intermediateFn2 : (string -> float -> unit) val intermediateFn3 : (float -> unit) val finalResult : unit = ()
يمكن أن يخبرك توقيع الوظيفة بعدد المعلمات التي تأخذها الوظيفة: ما عليك سوى حساب عدد الأسهم خارج الأقواس. إذا قبلت الدالة أو أعادت دالة أخرى ، فسيكون هناك المزيد من الأسهم ، ولكنها ستكون بين قوسين ويمكن تجاهلها. إليك بعض الأمثلة:
int->int->int // 2 int int string->bool->int // string, - bool, // int int->string->bool->unit // (int,string,bool) // (unit) (int->string)->int // , // ( int string) // int (int->string)->(int->bool) // (int string) // (int bool)
صعوبات المعلمات المتعددة
حتى تفهم المنطق الكامن وراء الكاري ، سوف ينتج عنه بعض النتائج غير المتوقعة. تذكر أنك لن تحصل على خطأ إذا قمت بتشغيل الدالة مع وسيطات أقل من المتوقع. بدلاً من ذلك ، تحصل على وظيفة مطبقة جزئيًا. إذا استخدمت بعد ذلك الوظيفة المطبقة جزئيًا في السياق حيث القيمة متوقعة ، يمكنك الحصول على خطأ غامض من المترجم.
فكر في وظيفة غير ضارة للوهلة الأولى:
// let printHello() = printfn "hello"
ما رأيك سيحدث إذا قمت باستدعاؤه كما هو موضح أدناه؟ هل ستتم طباعة "مرحبًا" على وحدة التحكم؟ حاول أن تخمن قبل التنفيذ. تلميح: انظر إلى توقيع الوظيفة.
// printHello
على عكس التوقعات ، لن يكون هناك اتصال. تتوقع الوظيفة الأصلية unit
كوسيطة لم يتم تمريرها. لذلك ، تم الحصول على دالة مطبقة جزئيًا (في هذه الحالة ، بدون حجج).
ماذا عن هذه الحالة؟ هل سيتم تجميعها؟
let addXY xy = printfn "x=%iy=%i" x x + y
إذا قمت بتشغيله ، فإن المترجم printfn
السطر مع printfn
.
printfn "x=%iy=%i" x //^^^^^^^^^^^^^^^^^^^^^ //warning FS0193: This expression is a function value, ie is missing //arguments. Its type is ^a -> unit.
إذا لم يكن هناك فهم للكاري ، يمكن أن تكون هذه الرسالة مشفرة للغاية. والحقيقة هي أن جميع التعبيرات التي يتم تقييمها بشكل منفصل (أي لا يتم استخدامها كقيمة إرجاع أو مرتبطة بشيء عن طريق "let") يجب تقييمها في قيمة unit
. في هذه الحالة ، لا يتم حسابها في قيمة unit
، ولكنها تُرجع دالة بدلاً من ذلك. هذا طريق طويل لقول أن printfn
يفتقد حجة.
في معظم الحالات ، تحدث مثل هذه الأخطاء عند التفاعل مع مكتبة من عالم .NET. على سبيل المثال ، يجب أن تأخذ طريقة TextReader
لفئة TextReader
معلمة unit
. يمكنك في كثير من الأحيان نسيان ذلك وعدم وضع الأقواس ، في هذه الحالة لا يمكنك الحصول على خطأ مترجم في وقت "المكالمة" ، ولكن سيظهر عند محاولة تفسير النتيجة كسلسلة.
let reader = new System.IO.StringReader("hello"); let line1 = reader.ReadLine // , printfn "The line is %s" line1 // // ==> error FS0001: This expression was expected to have // type string but here has type unit -> string let line2 = reader.ReadLine() // printfn "The line is %s" line2 //
في الكود أعلاه ، line1
هو مجرد مؤشر أو مفوض لأسلوب Readline
، وليس سلسلة ، كما قد تتوقع. reader.ReadLine()
استخدام ()
في reader.ReadLine()
إلى استدعاء الوظيفة فعليًا.
الكثير من الخيارات
يمكنك الحصول على رسائل مشفرة بالتساوي إذا قمت بتمرير العديد من المعلمات إلى دالة. بعض الأمثلة على تمرير الكثير من المعلمات printf
:
printfn "hello" 42 // ==> error FS0001: This expression was expected to have // type 'a -> 'b but here has type unit printfn "hello %i" 42 43 // ==> Error FS0001: Type mismatch. Expecting a 'a -> 'b -> 'c // but given a 'a -> unit printfn "hello %i %i" 42 43 44 // ==> Error FS0001: Type mismatch. Expecting a 'a->'b->'c->'d // but given a 'a -> 'b -> unit
على سبيل المثال ، في الحالة الأخيرة ، أفاد المترجم أنه من المتوقع وجود سلسلة تنسيق ذات ثلاث معلمات (التوقيع 'a -> 'b -> 'c -> 'd
له ثلاث معلمات) ، ولكن بدلاً من ذلك ، يتم استلام سلسلة بها اثنان (للتوقيع 'a -> 'b -> unit
معلمتين).
في الحالات التي لا يتم فيها استخدام printf
، غالبًا ما يعني تمرير عدد كبير من المعلمات أنه في مرحلة معينة من الحساب ، تم الحصول على قيمة بسيطة ، والتي يتم فيها تجربة المعلمة. سوف يستاء المترجم من أن القيمة البسيطة ليست دالة.
let add1 x = x + 1 let x = add1 2 3 // ==> error FS0003: This value is not a function // and cannot be applied
إذا قسمنا المكالمة العامة إلى سلسلة من الوظائف المتوسطة الصريحة ، كما فعلنا سابقًا ، يمكننا أن نرى ما الذي يحدث بشكل خاطئ بالضبط.
let add1 x = x + 1 let intermediateFn = add1 2 // let x = intermediateFn 3 //intermediateFn ! // ==> error FS0003: This value is not a function // and cannot be applied
موارد إضافية
هناك العديد من البرامج التعليمية لـ F # ، بما في ذلك المواد لأولئك الذين يأتون مع تجربة C # أو Java. قد تكون الروابط التالية مفيدة عندما تتعمق في F #:
كما تم وصف عدة طرق أخرى لبدء تعلم F # .
أخيرًا ، مجتمع F # ودود للغاية للمبتدئين. هناك محادثة نشطة للغاية في Slack ، تدعمها F # Software Foundation ، مع غرف للمبتدئين يمكنك الانضمام إليها بحرية . نوصي بشدة أن تفعل ذلك!
لا تنس زيارة موقع المجتمع الناطق باللغة الروسية F # ! إذا كان لديك أي أسئلة حول تعلم لغة ، يسعدنا مناقشتها في غرف الدردشة:
حول مؤلفي الترجمة
ترجمه kleidemos
تم إجراء تغييرات الترجمة والتحرير من خلال جهود المجتمع الناطق باللغة الروسية لمطوري F # . نشكر أيضًا schvepsss و shwars لإعداد هذه المقالة للنشر.