تذكر: البرمجة الوظيفية ليست برمجة بالوظائف! ! !
23.4 البرمجة الوظيفية
23.4.1 ما
هي البرمجة الوظيفية؟ إذا سألت بصراحة شديدة، فستجد أنه مفهوم ليس من السهل شرحه. لا يستطيع العديد من المحاربين القدامى الذين لديهم سنوات عديدة من الخبرة في مجال البرمجة أن يشرحوا بوضوح ما تدرسه البرمجة الوظيفية. البرمجة الوظيفية هي في الواقع مجال غير مألوف للمبرمجين الذين هم على دراية بالبرمجة الإجرائية. تبدو مفاهيم الإغلاق والاستمرار والكاريز غير مألوفة بالنسبة لنا. على الرغم من أن البرمجة الوظيفية تحتوي على نماذج رياضية جميلة لا يمكن للبرمجة الإجرائية مطابقتها، إلا أنها غامضة جدًا بحيث لا يستطيع إتقانها سوى أولئك الحاصلين على درجة الدكتوراه.
نصيحة: هذا القسم صعب بعض الشيء، لكنه ليس مهارة ضرورية لإتقان JavaScript إذا كنت لا ترغب في استخدام JavaScript لإكمال المهام التي يتم تنفيذها في Lisp، أو لا ترغب في تعلم المهارات الباطنية. البرمجة الوظيفية، يمكنك تخطيها والدخول إلى الفصل التالي من رحلتك.
لذا نعود إلى السؤال، ما هي البرمجة الوظيفية؟ الجواب طويل…
القانون الأول للبرمجة الوظيفية: الوظائف من النوع الأول.
كيف ينبغي فهم هذه الجملة نفسها؟ ما هو النوع الأول الحقيقي؟ دعونا نلقي نظرة على المفاهيم الرياضية التالية:
المعادلة الثنائية F(x, y) = 0, x, y متغيرات، اكتبها بالشكل y = f(x)، x معلمة، y هي القيمة المرجعة، f من x إلى y تسمى علاقة التعيين بالدالة. إذا كان هناك، G(x, y, z) = 0، أو z = g(x, y)، g هي علاقة التعيين من x، y إلى z، وهي أيضًا دالة. إذا كانت المعلمات x وy لـ g تحقق العلاقة السابقة y = f(x)، فإننا نحصل على z = g(x, y) = g(x, f(x)). x) هي دالة على x ومعلمة للدالة g. ثانيًا، g هي دالة ذات ترتيب أعلى من f.
بهذه الطريقة، نستخدم z = g(x, f(x)) لتمثيل الحل المرتبط للمعادلات F(x, y) = 0 وG(x, y, z) = 0، وهي دالة تكرارية . يمكننا أيضًا التعبير عن g بشكل آخر، تذكر z = g(x, y, f)، بحيث نعمم الدالة g في دالة ذات ترتيب أعلى. بالمقارنة مع النموذج السابق، فإن ميزة التمثيل الأخير هي أنه نموذج أكثر عمومية، مثل الحل المرتبط بـ T(x,y) = 0 وG(x,y,z) = 0. نحن أيضًا يمكن التعبير عنها بنفس الصيغة (فقط دع f=t). في نظام اللغة هذا الذي يدعم تكرار تحويل الحل لمشكلة إلى دالة ذات ترتيب أعلى، تسمى الوظيفة "النوع الأول".
من الواضح أن الوظائف في JavaScript هي "النوع الأول". هنا مثال نموذجي:
Array.prototype.each = الوظيفة (الإغلاق)
{
إرجاع this.length ? [closure(this[0])].concat(this.slice(1).each(closure)) : [];
}
هذا حقًا رمز سحري سحري، ويطلق العنان لسحر الأسلوب الوظيفي، ولا يوجد سوى وظائف ورموز في الكود بأكمله. إنها بسيطة في الشكل وقوية بلا حدود.
[1,2,3,4].each(function(x){return x * 2}) يحصل على [2,4,6,8]، بينما [1,2,3,4].each(function(x ){return x-1}) يحصل على [0,1,2,3].
جوهر الوظيفية والكائنية هو أن "تاو يتبع الطبيعة". إذا كان التوجه الشيئي عبارة عن محاكاة للعالم الحقيقي، فإن التعبير الوظيفي هو محاكاة للعالم الرياضي، بمعنى ما، فإن مستوى تجريده أعلى من التوجه الشيئي، لأن الأنظمة الرياضية بطبيعتها لها ميزات لا تضاهى في الطبيعة. من التجريد.
القانون الثاني للبرمجة الوظيفية: الإغلاقات هي أفضل صديق للبرمجة الوظيفية.
تعتبر عمليات الإغلاق، كما أوضحنا في الفصول السابقة، مهمة جدًا للبرمجة الوظيفية. أكبر ميزة لها هي أنه يمكنك الوصول مباشرة إلى البيئة الخارجية من الطبقة الداخلية دون تمرير المتغيرات (الرموز)، وهذا يوفر راحة كبيرة للبرامج الوظيفية في ظل التداخل المتعدد، وإليك مثال:
(function ExternalFun(x).
{
وظيفة الإرجاع الداخليةFun(y)
{
العودة س * ص؛
}
})(2)(3);
القانون الثالث للبرمجة الوظيفية: يمكن أن تكون الوظائف متجانسة.
ما هو الكاري؟ إنه مفهوم مثير للاهتمام. لنبدأ بالرياضيات: نقول، فكر في معادلة فضائية ثلاثية الأبعاد F(x, y, z) = 0، إذا حددنا z = 0، فسنحصل على F(x, y, 0) = 0، يُشار إليها بـ F '(س، ص). من الواضح هنا أن F' هي معادلة جديدة، والتي تمثل الإسقاط ثنائي الأبعاد لمنحنى الفضاء ثلاثي الأبعاد F(x, y, z) على المستوى z = 0. نشير إلى y = f(x, z)، دع z = 0، نحصل على y = f(x, 0)، نشير إليها كـ y = f'(x)، نقول أن الدالة f' هي حل كاريني لـ f .
ويرد أدناه مثال على JavaScript Currying:
وظيفة إضافة (س، ص)
{
if(x!=null && y!=null) return x + y;
آخر إذا (x!=null && y==null) وظيفة الإرجاع (y)
{
العودة س + ص؛
}
وإلا إذا (x==null && y!=null) وظيفة الإرجاع(x)
{
العودة س + ص؛
}
}
فار أ = إضافة(3, 4);
فار ب = إضافة(2);
var c = b(10);
في المثال أعلاه، ينتج عن b=add(2) دالة Currying لـ add()، وهي دالة للمعلمة y عندما تكون x = 2. لاحظ أنها تُستخدم أيضًا أعلاه من الإغلاقات.
ومن المثير للاهتمام أنه يمكننا تعميم الكاري على أي دالة، على سبيل المثال:
الدالة Foo(x, y, z, w)
{
فار args = الحجج؛
إذا (Foo.length < args.length)
وظيفة الإرجاع ()
{
يعود
args.callee.apply(Array.apply([], args).concat(Array.apply([], الوسيطات)));
}
آخر
العودة س + ص - ض * ث؛
}
القانون الرابع للبرمجة الوظيفية: تأخير التقييم والاستمرار.
// TODO: فكر في الأمر مرة أخرى هنا
23.4.2 مزايا
اختبار وحدة
البرمجة الوظيفيةكل رمز من رموز البرمجة الوظيفية الصارمة هو إشارة إلى كمية مباشرة أو نتيجة تعبيرية، وليس هناك أي وظيفة لها آثار جانبية. لأنه لا يتم تعديل القيمة أبدًا في مكان ما، ولا تقوم أي دالة بتعديل كمية خارج نطاقها تستخدمها وظائف أخرى (مثل أعضاء الفئة أو المتغيرات العامة). هذا يعني أن نتيجة تقييم الدالة هي فقط قيمة الإرجاع الخاصة بها، والأشياء الوحيدة التي تؤثر على قيمة الإرجاع هي معلمات الدالة.
هذا هو الحلم الرطب لاختبار الوحدة. بالنسبة لكل وظيفة في البرنامج قيد الاختبار، ما عليك سوى الاهتمام بمعلماتها، دون الحاجة إلى مراعاة ترتيب استدعاءات الوظائف، أو ضبط الحالة الخارجية بعناية. كل ما عليك فعله هو تمرير المعلمات التي تمثل حالات الحافة. إذا اجتازت كل وظيفة في البرنامج اختبار الوحدة، فهذا يعني أن لديك ثقة كبيرة في جودة البرنامج. لكن البرمجة الحتمية لا يمكن أن تكون متفائلة جدًا في Java أو C++، لا يكفي مجرد التحقق من القيمة المرجعة للدالة - يجب علينا أيضًا التحقق من الحالة الخارجية التي ربما تكون الدالة قد عدلتها.
تصحيح الأخطاء
إذا لم يعمل البرنامج الوظيفي بالطريقة التي تتوقعها، فإن تصحيح الأخطاء يعد أمرًا سهلاً. نظرًا لأن الأخطاء الموجودة في البرامج الوظيفية لا تعتمد على مسارات تعليمات برمجية غير مرتبطة بها قبل التنفيذ، فمن الممكن دائمًا إعادة إنتاج المشكلات التي تواجهها. في البرامج الأمرية تظهر الأخطاء وتختفي، لأن وظيفة الوظيفة تعتمد على الآثار الجانبية لوظائف أخرى، وقد تبحث لفترة طويلة في اتجاهات لا علاقة لها بحدوث الخطأ، ولكن دون أي نتائج. ليس هذا هو الحال مع البرامج الوظيفية - إذا كانت نتيجة إحدى الوظائف خاطئة، فبغض النظر عن ما قمت بتنفيذه من قبل، فإن الوظيفة ستعيد دائمًا نفس النتيجة الخاطئة.
بمجرد إعادة إنشاء المشكلة، سيكون العثور على سببها الجذري أمرًا سهلاً وقد يجعلك سعيدًا. قم بمقاطعة تنفيذ هذا البرنامج وفحص المكدس، كما هو الحال مع البرمجة الحتمية، يتم تقديم معلمات كل استدعاء دالة على المكدس. لكن هذه المعلمات ليست كافية في البرامج الحتمية، حيث تعتمد الوظائف أيضًا على متغيرات الأعضاء والمتغيرات العامة وحالة الفئة (والتي تعتمد بدورها على العديد من هذه المتغيرات). في البرمجة الوظيفية، تعتمد الوظيفة فقط على معلماتها، وهذه المعلومات موجودة أمام عينيك مباشرة! أيضًا، في البرامج الأمرية، مجرد التحقق من القيمة المرجعة لوظيفة ما لا يمكن التأكد من أن الوظيفة تعمل بشكل صحيح، يجب عليك التحقق من حالة العشرات من الكائنات خارج نطاق تلك الوظيفة للتأكيد. مع البرنامج الوظيفي، كل ما عليك فعله هو إلقاء نظرة على قيمته المرتجعة!
تحقق من المعلمات وقيم الإرجاع للوظيفة على طول المكدس، بمجرد العثور على نتيجة غير معقولة، أدخل تلك الوظيفة واتبعها خطوة بخطوة، وكرر هذه العملية حتى تجد النقطة التي تم إنشاء الخطأ فيها.
يمكن تنفيذ البرامج الوظيفية المتوازية بالتوازي دون أي تعديل. لا تقلق بشأن الجمود والأقسام الهامة لأنك لا تستخدم الأقفال أبدًا! لا يتم تعديل أي بيانات في برنامج وظيفي مرتين بواسطة نفس الخيط، ناهيك عن خيطين مختلفين. هذا يعني أنه يمكن إضافة سلاسل الرسائل ببساطة دون تفكير ثانٍ دون التسبب في المشكلات التقليدية التي تعاني منها التطبيقات المتوازية.
إذا كان الأمر كذلك، فلماذا لا يستخدم الجميع البرمجة الوظيفية في التطبيقات التي تتطلب عمليات متوازية للغاية؟ حسنا، إنهم يفعلون ذلك. صممت شركة Ericsson لغة وظيفية تسمى Erlang واستخدمتها في محولات الاتصالات السلكية واللاسلكية التي تتطلب قدرة عالية للغاية على تحمل الأخطاء وقابلية التوسع. اكتشف العديد من الأشخاص أيضًا مزايا Erlang وبدأوا في استخدامه. نحن نتحدث عن أنظمة التحكم في الاتصالات، والتي تتطلب موثوقية وقابلية للتوسع أكبر بكثير من النظام النموذجي المصمم لوول ستريت. في الواقع، نظام Erlang غير موثوق به وقابل للتوسيع، مثل JavaScript. أنظمة Erlang صلبة تمامًا.
لا تتوقف قصة التوازي عند هذا الحد، حتى لو كان برنامجك أحادي الخيط، فلا يزال بإمكان مترجم البرنامج الوظيفي تحسينه للعمل على وحدات معالجة مركزية متعددة. الرجاء إلقاء نظرة على الكود التالي:
String s1 = SomethingLongOperation1();
String s2 = someLongOperation2();
String s3 = concatenate(s1, s2);
في لغة برمجة وظيفية، يقوم المترجم بتحليل التعليمات البرمجية لتحديد الوظائف التي قد تستغرق وقتًا طويلاً والتي تنشئ السلاسل s1 وs2، ثم يقوم بتشغيلها بالتوازي. هذا غير ممكن في اللغات الأمرية، حيث قد تقوم كل وظيفة بتعديل الحالة خارج نطاق الوظيفة وقد تعتمد الوظائف اللاحقة على هذه التعديلات. في اللغات الوظيفية، يعد تحليل الوظائف تلقائيًا وتحديد المرشحين المناسبين للتنفيذ المتوازي أمرًا بسيطًا مثل تضمين الوظيفة تلقائيًا! وبهذا المعنى، تعتبر البرمجة ذات النمط الوظيفي "دليلًا على المستقبل" (حتى لو كنت لا أرغب في استخدام مصطلحات الصناعة، فسوف أقوم باستثناء هذه المرة). لم يعد مصنعو الأجهزة قادرين على جعل وحدات المعالجة المركزية تعمل بشكل أسرع، لذلك قاموا بزيادة سرعة نوى المعالج وحققوا زيادة في السرعة بمقدار أربعة أضعاف بسبب التوازي. بالطبع، نسوا أيضًا أن يذكروا أن الأموال الإضافية التي أنفقناها تم استخدامها فقط على البرامج لحل المشكلات الموازية. يمكن تشغيل نسبة صغيرة من البرامج الضرورية والبرامج الوظيفية بنسبة 100% مباشرة بالتوازي على هذه الأجهزة.
تم استخدامالنشر السريع للتعليمات البرمجية
للمطالبة بتثبيت التحديثات على نظام التشغيل Windows، ولم يكن من الممكن تجنب إعادة تشغيل الكمبيوتر، وأكثر من مرة، حتى لو تم تثبيت إصدار جديد من مشغل الوسائط. قام Windows XP بتحسين هذا الوضع بشكل كبير، لكنه لا يزال غير مثالي (لقد قمت بتشغيل Windows Update في العمل اليوم، والآن يظهر رمز مزعج دائمًا في الدرج ما لم أقوم بإعادة تشغيل الجهاز). تعمل أنظمة Unix دائمًا في وضع أفضل. عند تثبيت التحديثات، يجب إيقاف المكونات المرتبطة بالنظام فقط، وليس نظام التشغيل بأكمله. ومع ذلك، لا يزال هذا غير مرضٍ لتطبيق خادم واسع النطاق. يجب أن تكون أنظمة الاتصالات جاهزة للعمل بنسبة 100% من الوقت، لأنه في حالة فشل الاتصال بالطوارئ أثناء تحديث النظام، قد يؤدي ذلك إلى خسائر في الأرواح. لا يوجد سبب يدفع شركات وول ستريت إلى إغلاق الخدمات خلال عطلة نهاية الأسبوع لتثبيت التحديثات.
الوضع المثالي هو تحديث الكود ذي الصلة دون إيقاف أي مكون من مكونات النظام على الإطلاق. وهذا مستحيل في عالم حتمي. ضع في اعتبارك أنه عندما يقوم وقت التشغيل بتحميل فئة Java وتجاوز تعريف جديد، فإن جميع مثيلات هذه الفئة لن تكون متاحة بسبب فقدان حالتها المحفوظة. يمكننا أن نبدأ في كتابة بعض التعليمات البرمجية للتحكم في الإصدار الممل لحل هذه المشكلة، ثم إجراء تسلسل لجميع مثيلات هذه الفئة، وتدمير هذه المثيلات، ثم إعادة إنشاء هذه المثيلات بالتعريف الجديد لهذه الفئة، ثم تحميل البيانات المتسلسلة مسبقًا ونأمل أن يتم التحميل سيقوم الكود بنقل تلك البيانات بشكل صحيح إلى المثيل الجديد. علاوة على ذلك، يجب إعادة كتابة رمز النقل يدويًا لكل تحديث، ويجب توخي الحذر الشديد لمنع قطع العلاقات المتبادلة بين الكائنات. النظرية بسيطة، لكن الممارسة ليست سهلة.
بالنسبة للبرامج الوظيفية، يتم حفظ جميع الحالات، أي المعلمات التي تم تمريرها إلى الوظيفة، على المكدس، مما يجعل النشر السريع أمرًا سهلاً! في الواقع، كل ما يتعين علينا القيام به هو إجراء فرق بين كود العمل والإصدار الجديد، ثم نشر الكود الجديد. سيتم تنفيذ الباقي تلقائيًا بواسطة أداة اللغة! إذا كنت تعتقد أن هذه قصة خيال علمي، فكر مرة أخرى. لسنوات عديدة، كان مهندسو Erlang يقومون بتحديث أنظمة التشغيل الخاصة بهم دون انقطاع.
الاستدلال والتحسين بمساعدة الآلة
من الخصائص المثيرة للاهتمام للغات الوظيفية أنه يمكن الاستدلال بها رياضيًا. ولأن اللغة الوظيفية هي مجرد تطبيق لنظام رسمي، فإن جميع العمليات التي تتم على الورق يمكن تطبيقها على البرامج المكتوبة بهذه اللغة. يمكن للمترجمين استخدام النظرية الرياضية لتحويل جزء من التعليمات البرمجية إلى كود مكافئ ولكن أكثر كفاءة [7]. تخضع قواعد البيانات العلائقية لهذا النوع من التحسين لسنوات. لا يوجد سبب يمنع تطبيق هذه التقنية على البرامج العادية.
بالإضافة إلى ذلك، يمكنك استخدام هذه التقنيات لإثبات صحة أجزاء من برنامجك، وربما حتى إنشاء أدوات لتحليل التعليمات البرمجية الخاصة بك وإنشاء حالات الحافة تلقائيًا لاختبار الوحدة! هذه الوظيفة ليست ذات قيمة لنظام قوي، ولكن إذا كنت تقوم بتصميم جهاز تنظيم ضربات القلب أو نظام التحكم في الحركة الجوية، فلا غنى عن هذه الأداة. إذا كانت التطبيقات التي تكتبها ليست من المهام الأساسية في الصناعة، فإن هذا النوع من الأدوات يمكن أن يمنحك أيضًا بطاقة رابحة على منافسيك.
23.4.3 مساوئ البرمجة الوظيفية
الآثار الجانبية للإغلاقات
في البرمجة الوظيفية غير الصارمة، يمكن أن تتجاوز عمليات الإغلاق البيئة الخارجية (لقد رأينا ذلك بالفعل في الفصل السابق)، مما يؤدي إلى آثار جانبية، وعندما تحدث مثل هذه الآثار الجانبية بشكل متكرر ومتى تتغير البيئة التي يعمل فيها البرنامج بشكل متكرر، ويصبح من الصعب تتبع الأخطاء.
// TODO:
نموذج عودي
على الرغم من أن العودية غالبًا ما تكون أكثر أشكال التعبير إيجازًا، إلا أنها ليست بديهية مثل الحلقات غير العودية.
// TODO:
ضعف القيمة المتأخرة
// TODO: