تركيب الدوال (علوم الحاسوب)

في علم الحاسوب ، يُعدّ تركيب الدوال عملية أو آلية لدمج الدوال البسيطة لبناء دوال أكثر تعقيدًا. وكما هو الحال في تركيب الدوال في الرياضيات ، تُمرّر نتيجة كل دالة كمعامل للدالة التالية، وتكون نتيجة الدالة الأخيرة هي نتيجة العملية الكلية.

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

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

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

استدعاءات دالة التأليف

على سبيل المثال، لنفترض أن لدينا دالتين f و g ، كما في z = f ( y ) و y = g ( x ) . تركيبهما يعني أننا نحسب أولاً y = g ( x ) ، ثم نستخدم y لحساب z = f ( y ) . إليك المثال بلغة C :

float x , y , z ; // ... y = g ( x ); z = f ( y );

يمكن دمج الخطوات إذا لم نُعطِ اسمًا للنتيجة الوسيطة:

z = f ( g ( x ));

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

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

صديقتي

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

تسمية تركيب الدوال

لنفترض الآن أن الجمع بين استدعاء f() على نتيجة g() مفيد بشكل متكرر، والذي نريد تسميته foo() ليتم استخدامه كدالة في حد ذاته.

في معظم لغات البرمجة، يمكننا تعريف دالة جديدة يتم تنفيذها باستخدام التركيب. مثال في لغة C :

float foo ( float x ) { return f ( g ( x )); }

(الصيغة المطولة مع الوسائط ستفي بالغرض أيضاً.) مثال بلغة فورث :

 foo gf ;

في لغات مثل لغة C ، الطريقة الوحيدة لإنشاء دالة جديدة هي تعريفها في مصدر البرنامج، مما يعني أنه لا يمكن تركيب الدوال أثناء التشغيل . مع ذلك، يُمكن تقييم أي تركيبة من الدوال المُعرَّفة مسبقًا.

#include <stdio.h>typedef int FXN ( int );int f ( int x ) { return x + 1 ; } int g ( int x ) { return x * 2 ; } int h ( int x ) { return x - 3 ; }int eval ( FXN * fs [], int size , int x ) { for ( int i = 0 ; i < size ; i ++ ) x = ( * fs [ i ])( x );أعد x ؛ }int main () { // ((6 + 1) * 2) - 3 = 11 FXN * arr [] = { f , g , h }; printf ( "%d \n " , eval ( arr , 3 , 6 ));// ((6 - 3) * 2) + 1 = 7 arr [ 2 ] = f ; arr [ 0 ] = h ; printf ( "%d \n " , eval ( arr , 3 , 6 )); }

تأليف من الدرجة الأولى

في لغات البرمجة الوظيفية ، يمكن التعبير عن تركيب الدوال بشكل طبيعي باستخدام دالة أو عامل من الرتبة العليا . أما في لغات البرمجة الأخرى، فيمكنك كتابة آلياتك الخاصة لتنفيذ تركيب الدوال.

هاسكل

في لغة هاسكل ، يصبح المثال foo = fg المذكور أعلاه كالتالي:

foo = f . g

باستخدام عامل التركيب المدمج (.) والذي يمكن قراءته على أنه f بعد g أو g مركبة مع f .

يمكن تعريف عامل التركيب ∘ نفسه في لغة هاسكل باستخدام تعبير لامدا :

( . ) :: ( b -> c ) -> ( a -> b ) -> a -> c f . g = \ x -> f ( g x )

يصف السطر الأول نوع (.) - فهو يأخذ زوجًا من الدوال، f و g ، ويعيد دالة (تعبير لامدا في السطر الثاني). لاحظ أن لغة هاسكل لا تتطلب تحديد أنواع المدخلات والمخرجات الدقيقة لـ f و g؛ فالرموز a و b و c و x هي عناصر نائبة؛ العلاقة بين f و g هي المهمة فقط (يجب أن تقبل f ما تعيده g). هذا يجعل (.) عاملًا متعدد الأشكال .

عامل متعدد الأشكال ذو وسائط متعددة، في تدوين هاسكل الخالي من النقاط

f g x y

هو [ 3 ]

( . ) . ( . )

حيث لا تُعدّ أسماء المعاملات مهمة، [ 3 ] كما ذُكر أعلاه. يُنسب الفضل إلى ميخال شيفشيك في تقديم شرح منهجي لتركيب الدوال باستخدام معاملات إضافية إلى سي إس كالفاني. [ 4 ]

التلعثم

تُعدّ متغيرات لغة Lisp ، وخاصة Scheme ، وإمكانية تبادل التعليمات البرمجية والبيانات جنبًا إلى جنب مع معالجة الدوال، مناسبة للغاية للتعريف المتكرر للمشغل التركيبي المتغير .

( define ( compose.fs ) ( if ( null? fs ) ( lambda ( x ) x ) ; إذا لم يتم إعطاء أي وسيط، فسيتم تقييمها إلى دالة التطابق ( lambda ( x ) (( car fs ) (( apply compose ( cdr fs ) ) x ))))); أمثلة ( تعريف ( إضافة علامة تعجب str ) ( إلحاق سلسلة نصية str "!" ))( define givebang ( compose string->symbol add-a-bang symbol->string ))( givebang 'set ) ; ===> set!; تركيب مجهول (( تركيب جذر تربيعي - تربيع ) 5 ) ; ===> 0+5i

APL

تتضمن العديد من لهجات لغة APL خاصية تركيب الدوال المدمجة باستخدام الرمز . تعمل هذه الدالة ذات الرتبة الأعلى على توسيع نطاق تركيب الدوال ليشمل التطبيق الثنائي للدالة الموجودة على الجانب الأيسر بحيث A f∘g Bيكون A f g B.

foo f g

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

o { ⍺⍺ ⍵⍵ }

في اللهجة التي لا تدعم التعريف المضمن باستخدام الأقواس، يتوفر التعريف التقليدي:

r ( f o g ) x r f g x 

راكو

يحتوي Raku مثل Haskell على عامل تركيب الدوال المدمج، والفرق الرئيسي هو أنه يُكتب على النحو التالي: أو o.

my & foo = & f & g ;

كما هو الحال في لغة هاسكل، يمكنك تعريف المعامل بنفسك. في الواقع، إليك كود راكو المستخدم لتعريفه في تطبيق راكودو .

# يختلف التنفيذ قليلاً هنا لأنه يتحايل على صيغة proto sub infix :<∘> (&?, &?) is equiv(&[~]) is assoc<left> { * }multi sub infix :<∘> () { *. self } # يسمح لـ `[∘] @array` بالعمل عندما تكون `@array` فارغة multi sub infix :<∘> (&f) { & f } # يسمح لـ `[∘] @array` بالعمل عندما تحتوي `@array` على عنصر واحد multi sub infix :<∘> (&f, &g --> Block) { ( & f ) . count > 1 ?? -> | args { f | g | args } !! -> | args { f g | args } }# قم بتعريفه على أنه تهجئة "تكساس" (كل شيء أكبر، و ASCII في تكساس) بادئة & الخاصة بي : <o> : = بادئة & : <∘> ;

نيم

تدعم لغة Nim صيغة استدعاء الدوال الموحدة ، مما يسمح بتركيب الدوال بشكل عشوائي من خلال عامل صيغة الطريقة .. [ 5 ]

دالة foo ( a : عدد صحيح ): سلسلة نصية = $ a دالة bar ( a : سلسلة نصية ، count : عدد صحيح ): seq [ سلسلة نصية ] = for i in 0 .. < count : result.add ( a ) دالة baz ( a : seq [ سلسلة نصية ] ) = for i in a : echo i# مكافئ! echo foo ( 5 ) .bar ( 6 ) .baz () echo baz ( bar ( 6 , foo ( 5 )))

بايثون

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

من functools استورد reduce ومن typing استورد Callabledef compose ( * funcs ) -> Callable [[ int ], int ]: """تُركّب مجموعة من الدوال (f(g(h(...)))) في دالة مركبة واحدة.""" return reduce ( lambda f , g : lambda x : f ( g ( x )), funcs )# مثال : f = λx : x + 1 ، g = λx : x * 2 ، h = λx : x - 3# استدعاء الدالة x=10 : ((x - 3) * 2) + 1 = 15 print ( compose ( f , g , h )( 10 ))

جافا سكريبت

في لغة جافا سكريبت، يمكننا تعريفها كدالة تأخذ دالتين f و g ، وتنتج دالة:

دالة o ( f , g ) { return دالة ( x ) { return f ( g ( x )); } }// بدلاً من ذلك، باستخدام عامل الإرجاع وتعبيرات لامدا في ES2015: const compose = (... fs ) => ( x ) => fs . reduceRight (( acc , f ) => f ( acc ), x )

سي شارب

في لغة C#، يمكننا تعريفها كطريقة امتداد تأخذ الدوال f و g ، وتنتج دالة جديدة :

// مثال على الاستدعاء: // var c = f.ComposeWith(g); // // Func<int, bool> g = _ => ... // Func<bool, string> f = _ => ...public static Func < T1 , T3 > ComposeWith < T1 , T2 , T3 > ( this Func < T2 , T3 > f , Func < T1 , T2 > g ) => x => f ( g ( x ));

روبي

تتيح لك لغات مثل روبي إنشاء عامل ثنائي بنفسك:

class Proc def compose ( other_fn ) -> ( * as ) { other_fn.call ( call ( * as ) ) } end alias_method : + , :compose endf = -> ( x ) { x * 2 } g = -> ( x ) { x ** 3 } ( f + g ) .call ( 12 ) # = > 13824

ومع ذلك، تم تقديم عامل تركيب الدوال الأصلي في Ruby 2.6: [ 6 ]

f = proc { | x | x + 2 } g = proc { | x | x * 3 } ( f << g ) . call ( 3 ) # -> 11; مطابقة لـ f(g(3)) ( f >> g ) . call ( 3 ) # -> 15; مطابقة لـ g(f(3))

دراسة بحثية

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

التركيب واسع النطاق

يمكن التعامل مع البرامج أو الأنظمة بأكملها كدوال، والتي يمكن تركيبها بسهولة إذا كانت مدخلاتها ومخرجاتها محددة جيدًا. [ 7 ] وقد حققت خطوط الأنابيب التي تسمح بالتركيب السهل للمرشحات نجاحًا كبيرًا لدرجة أنها أصبحت نمطًا تصميميًا لأنظمة التشغيل.

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

انظر أيضاً

ملحوظات

  1. كوكس (1986) ، الصفحات 15-17
  2. DeMarco & Lister (1995) ، ص 133-135.
  3. 1 2 ميشال سيفتشيك https://melkornemesis.medium.com/haskell-explained-3f91658a67d3 (10 يناير 2021) هاسكل: (.) ​​. (.) وأوضح
  4. تشارلز سكالفاني https://gist.github.com/cscalfani/30ff149a75fc5580d1f8aec61f8e5283 (2017) التركيب الوظيفي مع معلمات متعددة في هاسكل
  5. "دليل لغة نيم: صيغة استدعاء الدوال" . nim-lang.org . تم الاطلاع عليه بتاريخ 17-08-2023 .
  6. "إصدار روبي 2.6.0" . www.ruby-lang.org . تاريخ الاسترجاع: 4 يناير 2019 .
  7. ريموند (2003)

مراجع