دخول
في
YOW! 2013 أحد مطوري لغة هاسكل ، أ
. أوضح
Philip Wadler كيف تسمح الأحاديات للغات الوظيفية الخالصة بتنفيذ عمليات ضرورية بشكل أساسي ، مثل المدخلات والمخرجات ومعالجة الاستثناءات. ليس من المستغرب أن يولد اهتمام الجمهور بهذا الموضوع نمواً هائلاً في المنشورات المتعلقة بالأشخاص الأحاديين على الإنترنت. لسوء الحظ ، تستخدم معظم هذه المنشورات أمثلة مكتوبة بلغات وظيفية ، مما يعني أن القادمين الجدد إلى البرمجة الوظيفية يريدون التعرف على الموناديات. لكن الأحاديات ليست خاصة بـ Haskell أو اللغات الوظيفية ، وقد تتضح من خلال أمثلة بلغات البرمجة الضرورية. هذا هو الغرض من هذا الدليل.
كيف يختلف هذا الدليل عن الباقي؟ سنحاول فتح monads في مدة لا تزيد عن 15 دقيقة باستخدام الحدس فقط وبعض الأمثلة الأولية لرمز Python. لذلك ، لن نبدأ في وضع النظريات والتعمق في الفلسفة ، ومناقشة
البوريتو ،
والبدلات الفضائية ،
والمكاتب والبدائل الداخلية.
أمثلة تحفيزية
سننظر في ثلاث مسائل تتعلق بتكوين الوظائف. سنحلها بطريقتين: الضرورة المعتادة واستخدام الموناد. ثم نقارن الطرق المختلفة.
1. قطع الأشجار
افترض أن لدينا ثلاث وظائف أحادية:
f1
و
f2
و
f3
، والتي تأخذ رقماً وتعيدها بمقدار 1 و 2 و 3 على التوالي. كل وظيفة أيضا بإنشاء رسالة ، وهو تقرير عن العملية المكتملة.
def f1(x): return (x + 1, str(x) + "+1") def f2(x): return (x + 2, str(x) + "+2") def f3(x): return (x + 3, str(x) + "+3")
نود تجميعها معًا لمعالجة المعلمة
x
، بمعنى آخر ، نود حساب
x+1+2+3
. بالإضافة إلى ذلك ، نحتاج إلى الحصول على شرح يمكن قراءته من قبل الإنسان حول ما قامت به كل وظيفة.
يمكننا تحقيق النتيجة التي نحتاجها بالطريقة التالية:
log = "Ops:" res, log1 = f1(x) log += log1 + ";" res, log2 = f2(res) log += log2 + ";" res, log3 = f3(res) log += log3 + ";" print(res, log)
هذا الحل ليس مثاليًا ، لأنه يتكون من عدد كبير من البرامج الوسيطة الرتيبة. إذا أردنا إضافة وظيفة جديدة إلى سلسلتنا ، فسنضطر إلى تكرار رمز الارتباط هذا. بالإضافة إلى ذلك ، فإن التلاعب بمتغيرات
res
والسجل يضعف من قراءة التعليمات البرمجية ، مما يجعل من الصعب اتباع المنطق الرئيسي للبرنامج.
من الناحية المثالية ، يجب أن يبدو البرنامج كسلسلة بسيطة من الوظائف ، مثل
f3(f2(f1(x)))
. لسوء الحظ ، لا تتطابق أنواع البيانات التي يتم إرجاعها بواسطة
f1
و
f2
مع أنواع المعلمات
f2
و
f3
. ولكن يمكننا إضافة وظائف جديدة إلى السلسلة:
def unit(x): return (x, "Ops:") def bind(t, f): res = f(t[0]) return (res[0], t[1] + res[1] + ";")
الآن يمكننا حل المشكلة على النحو التالي:
print(bind(bind(bind(unit(x), f1), f2), f3))
يوضح المخطط التالي العملية الحسابية التي تحدث عند
x=0
. هنا
v1
و
v2
و
v3
هي القيم التي تم الحصول عليها نتيجة لاستدعاءات
unit
bind
.

تقوم وظيفة
unit
بتحويل معلمة الإدخال
x
إلى مجموعة من الأرقام وسلسلة. تقوم دالة
bind
باستدعاء الوظيفة التي تم تمريرها اليها كمعلمة وتجميع النتيجة في المتغير الوسيط
t
.
كنا قادرين على تجنب تكرار الوسيطة من خلال وضعها في وظيفة
bind
. الآن ، إذا حصلنا على الوظيفة
f4
، فإننا فقط
f4
في السلسلة:
bind(f4, bind(f3, ... ))
ونحن لسنا بحاجة لإجراء أي تغييرات أخرى.
2. قائمة القيم المتوسطة
سنبدأ هذا المثال أيضًا بوظائف أحادية بسيطة.
def f1(x): return x + 1 def f2(x): return x + 2 def f3(x): return x + 3
كما في المثال السابق ، نحتاج إلى تكوين هذه الوظائف من أجل حساب
x+1+2+3
. نحتاج أيضًا إلى الحصول على قائمة بجميع القيم التي تم الحصول عليها نتيجة عمل وظائفنا ، وهي
x
و
x+1
و
x+1+2
و
x+1+2+3
.
على عكس المثال السابق ، فإن وظائفنا قابلة للتكوين ، أي أن أنواع معلمات الإدخال الخاصة بها تتزامن مع نوع النتيجة. لذلك ، سلسلة بسيطة
f3(f2(f1(x)))
بإرجاع النتيجة النهائية. لكن في هذه الحالة ، نفقد القيم الوسيطة.
دعنا نحل المشكلة "وجهاً لوجه":
lst = [x] res = f1(x) lst.append(res) res = f2(res) lst.append(res) res = f3(res) lst.append(res) print(res, lst)
لسوء الحظ ، يحتوي هذا الحل أيضًا على الكثير من البرامج الوسيطة. وإذا قررنا إضافة
f4
، فسيتعين علينا تكرار هذا الرمز مرة أخرى للحصول على قائمة القيم الوسيطة الصحيحة.
لذلك ، نضيف وظيفتين إضافيتين ، كما في المثال السابق:
def unit(x): return (x, [x]) def bind(t, f): res = f(t[0]) return (res, t[1] + [res])
نعيد الآن كتابة البرنامج كسلسلة من المكالمات:
print(bind(bind(bind(unit(x), f1), f2), f3))
يوضح المخطط التالي العملية الحسابية التي تحدث عند
x=0
. مرة أخرى ، تشير
v1
و
v2
و
v3
إلى القيم التي تم الحصول عليها من
unit
bind
المكالمات.

3. القيم الفارغة
دعونا نحاول إعطاء مثال أكثر إثارة للاهتمام ، هذه المرة مع الطبقات والكائنات. افترض أن لدينا فئة
Employee
مع طريقتين:
class Employee: def get_boss(self):
كل كائن من فئة
Employee
لديه مدير (كائن آخر من فئة
Employee
) وراتب ، والتي يتم الوصول إليها من خلال الطرق المناسبة. يمكن أيضًا إرجاع كلا الطريقتين بلا (الموظف ليس لديه مدير ، الراتب غير معروف).
في هذا المثال ، سننشئ برنامجًا يوضح راتب زعيم هذا الموظف. إذا لم يتم العثور على المدير ، أو لا يمكن تحديد راتبه ، فإن البرنامج سيعود بلا.
من الناحية المثالية ، نحن بحاجة إلى كتابة شيء من هذا القبيل
print(john.get_boss().get_wage())
ولكن في هذه الحالة ، إذا
None
تُرجع أي من الطرق ،
None
برنامجنا بخطأ.
قد تبدو الطريقة الواضحة للتعامل مع هذا الموقف كما يلي:
result = None if john is not None and john.get_boss() is not None and john.get_boss().get_wage() is not None: result = john.get_boss().get_wage() print(result)
في هذه الحالة ، نسمح بمكالمات إضافية إلى
get_wage
و
get_wage
. إذا كانت هذه الطرق ثقيلة بما فيه الكفاية (على سبيل المثال ، الوصول إلى قاعدة البيانات) ، فلن يكون حلنا مفيدًا. لذلك ، نغيره:
result = None if john is not None: boss = john.get_boss() if boss is not None: wage = boss.get_wage() if wage is not None: result = wage print(result)
هذا الكود هو الأمثل من حيث الحساب ، ولكن قراءة سيئة بسبب ثلاثة
if
المتداخلة. لذلك ، سنحاول استخدام نفس الخدعة كما في الأمثلة السابقة. تحديد وظيفتين:
def unit(e): return e def bind(e, f): return None if e is None else f(e)
والآن يمكننا وضع الحل بالكامل في سطر واحد.
print(bind(bind(unit(john), Employee.get_boss), Employee.get_wage))
كما لاحظت بالفعل ، في هذه الحالة ، لم يكن علينا أن نكتب وظيفة
unit
: إنها ببساطة تُرجع معلمة الإدخال. لكننا سنترك الأمر حتى يسهل علينا تعميم تجربتنا.
لاحظ أيضًا أنه في Python يمكننا استخدام الأساليب
john.get_boss()
، مما سمح لنا بكتابة
Employee.get_boss(john)
بدلاً من
john.get_boss()
.
يعرض الرسم البياني التالي عملية الحساب عندما لا يكون لـ John قائد ، بمعنى
john.get_boss()
None
يُرجع إلى
None
.

النتائج
لنفترض أننا نريد دمج وظائف نفس النوع
f1
،
f2
،
…
،
fn
. إذا كانت معلمات الإدخال هي نفس النتائج ، فيمكننا استخدام سلسلة بسيطة من النموذج
fn(… f2(f1(x)) …)
. يُظهر الرسم البياني التالي عملية حساب معممة بنتائج وسيطة ، يُشار إليها على أنها
v1
،
v2
،
…
،
vn
.

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

قبل استدعاء
f1
قمنا ببعض التهيئة. في المثال الأول ، قمنا بتهيئة متغير لتخزين سجل شائع ، في الثانية للحصول على قائمة بالقيم الوسيطة. بعد ذلك ، قمنا باستدعاء استدعاءات الوظائف برمز اتصال معين: قمنا بحساب القيم التجميعية ، والتحقق من النتيجة بلا.
2. مونادس
كما رأينا في الأمثلة ، عانت القرارات الحتمية دائمًا من الحيرة والتكرار والمنطق المربك. للحصول على رمز أكثر أناقة ، استخدمنا نمط تصميم معين ، والذي أنشأنا وفقًا له وظيفتين:
unit
bind
. يسمى هذا القالب
الموناد . تحتوي وظيفة
bind
على برنامج وسيط بينما تنفذ
unit
التهيئة. هذا يتيح لنا تبسيط الحل النهائي لسطر واحد:
bind(bind( ... bind(bind(unit(x), f1), f2) ... fn-1), fn)
يمكن تمثيل عملية الحساب بواسطة مخطط:

استدعاء
unit(x)
يولد قيمة أولية من
v1
. ثم يُنشئ
bind(v1, f1)
قيمة متوسطة جديدة
v2
، والتي تُستخدم في الاستدعاء التالي
bind(v2, f2)
. تستمر هذه العملية حتى يتم الحصول على النتيجة النهائية. من خلال تحديد
unit
المختلفة
bind
ضمن إطار هذا القالب ، يمكننا دمج الوظائف المختلفة في سلسلة واحدة من العمليات الحسابية. تحتوي مكتبات
Monad (
على سبيل المثال ، PyMonad أو OSlash ، تقريبًا. ) عادةً على
monads جاهزة للاستخدام (أزواج من
unit
والوظيفة المرتبطة) لتنفيذ تركيبات معينة من الوظائف.
لتسلسل الدوال ، يجب أن تكون القيم التي يتم إرجاعها بواسطة
unit
bind
من نفس النوع كمعلمات الإدخال
bind
. هذا النوع يسمى
monadic . بالنسبة إلى المخطط أعلاه ، يجب أن يكون نوع المتغيرات
v1
،
v2
،
…
،
vn
من النوع الأحادي.
أخيرًا ، فكر في كيفية تحسين التعليمات البرمجية المكتوبة باستخدام الموناديات. من الواضح أن مكالمات
bind
المتكررة تبدو غير أنيقة. لتجنب ذلك ، حدد وظيفة خارجية أخرى:
def pipeline(e, *functions): for f in functions: e = bind(e, f) return e
الآن بدلا من ذلك
bind(bind(bind(bind(unit(x), f1), f2), f3), f4)
يمكننا استخدام الاختصار التالي:
pipeline(unit(x), f1, f2, f3, f4)
استنتاج
Monads هي نمط تصميم بسيط وقوي يستخدم لإنشاء وظائف. في لغات البرمجة التصريحية ، فإنه يساعد على تنفيذ آليات حتمية مثل التسجيل أو الإدخال / الإخراج. بلغات حتمية
يساعد على تعميم وتقصير التعليمات البرمجية التي تربط سلسلة من المكالمات من وظائف من نفس النوع.
توفر هذه المقالة فقط فهم سطحي ، من monads. يمكنك معرفة المزيد عن طريق الاتصال بالمصادر التالية:
- ويكيبيديا
- Monads في Python (مع بناء جملة لطيف!)
- مناد الدروس الجدول الزمني