تقنية اللامدا Lambda في لغة محرك Godot

GDscript

في هذه المقالة، أشرح مفهوم جديد وهو دالة اللامدا Lambda function وهو تابع ومتعلق بشكل كبير لمفهوم قابلية الاستدعاء-Callable  الذي تم شرحه سابقًا، لذا من الأفضل أن تراجع المقالة السابقة قبل أن تواصل قراءة هذه المقالة.

وفيه قد تعلمنا كيف يمكن لمفهوم الدوال القابلة للاستدعاء أن يمكّننا من تمرير واستدعاء الدوال داخل دوال أخرى بسهولة. 

عوائق قابلية الاستدعاء

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

هناك أوقات تحتاج فيها إلى تمرير دالة داخل دالة أخرى، مثل عند ربط الإشارات Signals, أو استخدام عقدة الوسيط Tween, أو ترتيب المصفوفات. عادةً ما يتم إنشاء هذه الدالة في مكان آخر في الملف، مما قد يسبب بعض المشاكل. على سبيل المثال، إذا لم تكن تستخدم الدالة في أي مكان آخر، فستظل متاحة للاستخدام، حتى لو لم تعد بحاجة إليها. يمكن أن يؤدي هذا إلى صعوبة قراءة الملف وفهمه، فقد لا يتضح من أين تأتي كل هذه الدوال.

–  George Marques (vnen)

وأحاول شرح ما يقصده في بعض الأمثلة العملية البسيطة
بالنسبة للإشارات فعندما كنت تربط إشارة داخل مشهد معين كنت تمرر دالة داخل دالة الاتصال connect الخاصة بربط الإشارات هكذا : 

var enemy = Enemy.new()
func _ready()
  enemy.take_damage_signal.connect(enemy_hurt)
  enemy.take_damage_signal.emit()

# هذه الدالة تم استخدامها مرة واحدة فقط ولن يتم استخدامها  مجددًا
func enemy_hurt():
  print(‘enemy hurt’)

لاحظ كيف قمت بتمرير دالة قابلة للاستدعاء مثل العدو_تأذى enemy_hurt داخل دالة الاتصال connect المسؤولة عن
ربط إشارة_تلقي_الضرر take_damage_signal الخاصة بالعدو داخل المشهد  الحالي.

الدالة العدو_تأذى اُستخدمت مرة واحدة فقط ولن يتم استخدامها مرة أخرى, قد تم إنشاؤها فقط لكي توضع داخل دالة الاتصال الخاصة بالاشارة.

لكن هل ستستعملها  مجددًا ؟ الاجابة لا لن يتم استخدامها مرة اخرى في أي مكان آخر. إذا لماذا نبقيها في الملف على الرغم من أننا لن نحتاج إليها مرة أخرى؟ هذا ما يسبب مشكلة في قراءة الملف وفهمه.

بالنسبة لعقدة الوسيط Tween، يمكنك تمرير دالة قابلة للاستدعاء داخل أحد دوالها. مثال:

func _ready():
  var tween = get_tree().create_tween().set_loops()    
  tween.tween_callback(shoot).set_delay(1)

func shoot():
  print(‘shoot’)

هل لاحظت نفس المشكلة هنا؟ أنشأت دالة مسؤولة عن إطلاق شيء ما واستخدمتها مرة واحدة كدالة قابلة للاستدعاء داخل دالة tween_callback.

سل نفسك نفس السؤال السابق هل ستستعملها  مجددًا ؟ الاجابة لا لن يتم استخدامها مرة اخرى في أي مكان آخر  

وأمثلة كثيرة مثل تلك أدت للتفكير في ابتكار طريقة لإنشاء دالة لمرة واحدة فقط داخل نطاق معين وتُستخدم داخل هذا النطاق ثم تختفي عن الوجود، هنا  ظهر مفهوم جديد وهو دالة اللامدا Lambda

إنشاء دالة لامدا

أولًا، دعنا نتعرف على كيفية إنشاء دالة اللامدا في لغة غودو. هذا الأمر أسهل مما قد تتصور.

var lambda = func (x, y): return x + y
print(lambda.call(1, 2)) # 3
print(lambda.call(3, 2)) # 5
print(lambda.call(5, 7)) # 12

لاحظ كيف يتم تعريف اللامدا، قمت بإنشاء متغير يُدعى lambda وجعلته يساوي الدالة نفسها بهذا الشكل، كما ترى.

المتغير هو في الحقيقة كائن من نوع قابلية الاستدعاء Callable، لأن اللامدا هو شكل آخر أو تجسيد آخر لصنف قابلية الاستدعاء، بالتالي كل ما تعلمته في درس قابلية الاستدعاء ينطبق تماما على اللامدا.

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

var lambda = func (x, y):
  var z = x + y
  return z

دالة اللامدا هي دالة مجهولة

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

var lambda = func (x, y): return x + y

الجزء الأيمن من المعادلة هو ما نسميه “دالة اللامدا”، بينما الجزء الأيسر منه مجرد متغير أو كائن يمثل هذه الدالة.

دالة اللامدا تصنف كدالة مجهولة، أي دالة بدون اسم، وإذا فكرت في الأمر، تجد أن إعطاء اسم لهذه الدالة لن يكون له فائدة هنا، لأنه في النهاية تُخزن في متغير وهو ما يمثلها. لذلك، إعطاء اسم لهذه الدالة ليس ضروريًا.

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

على سبيل المثال:

# يمكننا وضع اسم لدالة اللامدا لكنه اختياري
var lambda = func sum (x, y): return x + y

ما الذي أضافته؟

إذا كانت دالة اللامدا هي شكل آخر لكتابة الكائنات من نوع صنف قابلية الاستدعاء, فما الجوهر الذي أضافته أو ميزتها عن الشكل الاعتيادي الذي تعلمته في درس قابلية الاستدعاء؟

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

هل تتذكر هذا المثال من الدرس السابق؟

func sum(x, y):
  return x + y

func applyOperation(x, y, obj: Callable):
  var result = obj.call(x, y)
  return result

كان لديك هنا دالة تنفيذ الإجراء applyOperation(x, y, obj) التي تستقبل القيم x و y والمعامل obj وهو كائن من نوع قابلية الاستدعاء.
ثم كنت تستدعى call من الكائن obj وتُرسل القيم x و y فيه. كنت تقوم بإنشاء دالة خارجية مثل دالة الجمع على سبيل المثال وتمررها كمعامل إلى دالة تنفيذ الإجراء:

var result = applyOperation(5, 10, sum)
print(result) # return 15

الآن، انظر كيف تكون الأمور مع دالة اللامدا:

func applyOperation(x, y, obj: Callable):
  var result = obj.call(x, y)
  return result

# لاحظ كيف تم انشاء دالة داخل المعامل مباشرةً
var result = applyOperation(5, 10, func (x, y): return x + y)
print(result) # return 15

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

هذا يعود إلى أن دالة اللامدا فهي دالة تمكنك بإنشاؤها داخل المعاملات مباشرةً, وهذه من أهم خصائصها التي ذكرتها، وإذا فكرت في الأمر، تجد أن دالة اللامدا تعتبر من نوع الصنف قابلية الاستدعاء لذا تستقبلها دالة تنفيذ الإجراء كمعامل من نوع Callable وتسندها للكائن obj دون مشاكل.

استخدامها كبديل لحل الصعوبات التي واجهتنا مع الـ Callable

هل تتذكر الصعوبات التي ذكرتها  في بداية هذه المقالة والأمثلة التي عرضتها وكيف واجهت مشكلة بأنك تقوم بإنشاء دالة كاملة في مكان ما وتستخدمها لمرة واحدة فقط وعدم استخدامها مرة أخرى في أي مكان آخر؟ 

وقلت أنك إن كنت تريد أن تبتكر طريقة لإنشاء دالة لمرة واحدة فقط داخل نطاق معين وتستخدم داخل هذا النطاق ثم تختفي عن الوجود. هل تفكر فيما افكر؟ هل يذكرك هذا بميزة موجودة داخل دوال اللامدا؟

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

سأعيد لك عرض نفس الأمثلة السابقة مع تنفيذ ميزة دالة اللامدا عليها، مع إضافة أمثلة عملية أخرى:

var enemy = Enemy.new()
func _ready():    
  enemy.take_damage_signal.connect(func ():  print(‘enemy hurt’))    
  enemy.take_damage_signal.emit()

انظر كيف قمت بإنشاء دالة اللامدا داخل دالة الاتصال الخاصة بالإشارة ولم نضطر حتى إلى إنشاء دالة خارجية وتمريرها.

func _ready():
  var tween = get_tree().create_tween().set_loops()
  tween.tween_callback(func (): print(‘shoot’)).set_delay(1)

وهنا أيضًا نفس الأمر، تجد الأمور أصبحت اسهل وأكثر نظافة.

أيضًا، أي دالة تجدها تستقبل كائن من نوع الصنف قابلية الاستدعاء Callable، يمكنك استخدام وإنشاء دالة اللامدا فيه ببساطة. على سبيل المثال، بعض دوال المصفوفة تستقبل كائن من نوع الصنف قابلية الاستدعاء مثل دالة filter:

func _ready():    
  var arr = [1, 4, 11, 5, 8, 6, 9, 3]
  #  نريد الأعداد الزوجية فقط (الأعداد التي تقبل القسمة على 2)
  var result = arr.filter(func(x): return x % 2 == 0)
  print(result) # [4, 8, 6]

انظر كيف قمنا بعمل ما نريده بشكل مباشر دون تكلفة إنشاء دالة خارجية أخرى.

المصادر

0 0 votes
Article Rating
Subscribe
نبّهني عن
guest

1 تعليق
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
عبسي
عبسي
5 شهور

با اخي والله انكم اساطيرر استمروو