الخيار / ربما ، وإما ، ومستقبل Monads في JavaScript ، و Python ، و Ruby ، و Swift ، و Scala
نشرت: 2022-03-11يقدم هذا البرنامج التعليمي monad شرحًا موجزًا للأحادية ويوضح كيفية تنفيذ أكثرها فائدة في خمس لغات برمجة مختلفة - إذا كنت تبحث عن monads في JavaScript ، و monads في Python ، و monads في Ruby ، و monads في Swift ، و / أو monads في Scala ، أو لمقارنة أي تطبيقات ، فأنت تقرأ المقالة الصحيحة!
باستخدام هذه الأحاديات ، ستتخلص من سلسلة من الأخطاء مثل استثناءات المؤشر الصفري ، والاستثناءات التي لم تتم معالجتها ، وظروف السباق.
هذا ما أغطيه أدناه:
- مقدمة في نظرية الفئة
- تعريف monad
- تطبيقات الخيار ("ربما") monad ، أو monad ، و Future monad ، بالإضافة إلى نموذج برنامج يستفيد منها ، في JavaScript و Python و Ruby و Swift و Scala
هيا بنا نبدأ! محطتنا الأولى هي نظرية الفئة ، والتي هي أساس الموناد.
مقدمة في نظرية الفئات
نظرية الفئة هي مجال رياضي تم تطويره بنشاط في منتصف القرن العشرين. الآن هو أساس العديد من مفاهيم البرمجة الوظيفية بما في ذلك monad. دعنا نلقي نظرة سريعة على بعض مفاهيم نظرية الفئات ، مضبوطة لمصطلحات تطوير البرمجيات.
إذن هناك ثلاثة مفاهيم أساسية تحدد الفئة:
- الكتابة هي تمامًا كما نراها في اللغات المكتوبة بشكل ثابت. أمثلة:
Int
،String
،Dog
،Cat
، إلخ. - وظائف تربط نوعين. لذلك ، يمكن تمثيلها كسهم من نوع إلى نوع آخر ، أو لأنفسهم. يمكن الإشارة إلى الوظيفة $ f $ من النوع $ T $ إلى النوع $ U $ كـ $ f: T \ to U $. يمكنك التفكير في الأمر كدالة لغة برمجة تأخذ وسيطة من النوع $ T $ وتعيد قيمة من النوع $ U $.
- التركيب هو عملية ، يُشار إليها بواسطة عامل التشغيل $ \ cdot $ ، والتي تبني وظائف جديدة من الوظائف الموجودة. في فئة ما ، يكون مضمونًا دائمًا لأي وظائف $ f: T \ to U $ و $ g: U \ to V $ توجد وظيفة فريدة $ h: T \ to V $. يشار إلى هذه الوظيفة على أنها $ f \ cdot g $. تعمل العملية بشكل فعال على تعيين زوج من الوظائف إلى وظيفة أخرى. في لغات البرمجة ، هذه العملية ، بالطبع ، ممكنة دائمًا. على سبيل المثال ، إذا كانت لديك دالة تُرجع طول سلسلة - $ strlen: String \ to Int $ —وظيفة تخبر ما إذا كان الرقم زوجيًا - $ even: Int \ to Boolean $ - فيمكنك إنشاء الدالة $ even {\ _} strlen: String \ to Boolean $ والتي توضح ما إذا كان طول
String
. في هذه الحالة $ even {\ _} strlen = even \ cdot strlen $. يتضمن التكوين سمتين:- الارتباط: $ f \ cdot g \ cdot h = (f \ cdot g) \ cdot h = f \ cdot (g \ cdot h) $
- وجود دالة هوية: $ \ forall T: \ موجود f: T \ to T $ ، أو بلغة إنجليزية بسيطة ، لكل نوع $ T $ توجد دالة تقوم بتعيين $ T $ لنفسها.
لذلك دعونا نلقي نظرة على فئة بسيطة.
ملاحظة جانبية: نفترض أن Int
و String
وجميع الأنواع الأخرى هنا مضمونة على أنها غير خالية ، أي أن القيمة الخالية غير موجودة.
ملاحظة جانبية 2: هذا في الواقع جزء فقط من فئة ، ولكن هذا كل ما نريده لمناقشتنا ، لأنه يحتوي على جميع الأجزاء الأساسية التي نحتاجها والمخطط أقل تشوشًا بهذه الطريقة. قد تحتوي الفئة الحقيقية أيضًا على جميع الوظائف المكونة مثل $ roundToString: Double \ to String = intToString \ cdot round $ ، لتلبية بند تكوين الفئات.
قد تلاحظ أن الوظائف في هذه الفئة بسيطة للغاية. في الواقع يكاد يكون من المستحيل وجود خطأ في هذه الوظائف. لا توجد أصفار ، ولا استثناءات ، فقط العمليات الحسابية والعمل بالذاكرة. لذا فإن الشيء السيئ الوحيد الذي يمكن أن يحدث هو فشل المعالج أو الذاكرة - وفي هذه الحالة تحتاج إلى تعطل البرنامج على أي حال - ولكن هذا نادرًا ما يحدث.
ألن يكون من الجيد أن تعمل جميع الكودات لدينا على هذا المستوى من الاستقرار؟ إطلاقا! ولكن ماذا عن I / O ، على سبيل المثال؟ بالتأكيد لا يمكننا العيش بدونها. هنا يأتي دور الحلول الأحادية للإنقاذ: فهي تعزل جميع العمليات غير المستقرة إلى أجزاء صغيرة جدًا ومدققة جيدًا من التعليمات البرمجية - ثم يمكنك استخدام حسابات مستقرة في تطبيقك بالكامل!
أدخل Monads
دعنا نسمي السلوك غير المستقر مثل I / O أحد الآثار الجانبية . الآن نريد أن نكون قادرين على العمل مع جميع وظائفنا المحددة مسبقًا مثل length
والأنواع مثل String
بطريقة مستقرة في وجود هذا التأثير الجانبي .
لذلك لنبدأ بفئة فارغة $ M [A] $ ونجعلها في فئة تحتوي على قيم بنوع معين من الآثار الجانبية وأيضًا القيم بدون آثار جانبية. لنفترض أننا حددنا هذه الفئة وهي فارغة. في الوقت الحالي ، لا يوجد شيء مفيد يمكننا فعله به ، لذلك لجعله مفيدًا ، سنتبع الخطوات الثلاث التالية:
- املأها بقيم الأنواع من الفئة $ A $ ، مثل
String
، وInt
، وDouble
، وما إلى ذلك (المربعات الخضراء في الرسم التخطيطي أدناه) - بمجرد أن نحصل على هذه القيم ، ما زلنا لا نستطيع فعل أي شيء ذي معنى معها ، لذلك نحتاج إلى طريقة لأخذ كل دالة $ f: T \ to U $ من $ A $ وإنشاء دالة $ g: M [T] \ to M [U] $ (الأسهم الزرقاء في الرسم البياني أدناه). بمجرد أن نحصل على هذه الوظائف ، يمكننا فعل كل شيء باستخدام القيم الموجودة في الفئة $ M [A] $ والتي تمكنا من القيام بها في الفئة $ A $.
- الآن بعد أن أصبح لدينا فئة $ M [A] $ جديدة ، ظهرت فئة جديدة من الوظائف بتوقيع $ h: T \ to M [U] $ (الأسهم الحمراء في الرسم البياني أدناه). تظهر كنتيجة لتعزيز القيم في الخطوة الأولى كجزء من قاعدة الكود لدينا ، أي نكتبها حسب الحاجة ؛ هذه هي الأشياء الرئيسية التي ستميز العمل بـ $ M [A] $ مقابل العمل بـ $ A $. ستكون الخطوة الأخيرة هي جعل هذه الوظائف تعمل جيدًا على الأنواع الموجودة في $ M [A] $ أيضًا ، أي القدرة على اشتقاق الوظيفة $ m: M [T] \ إلى M [U] $ من $ h: T \ إلى M [U] $
لنبدأ بتعريف طريقتين لترقية قيم أنواع $ A $ إلى قيم $ M [A] $ من الأنواع: وظيفة واحدة بدون آثار جانبية وأخرى لها آثار جانبية.
- الأول يسمى $ pure $ ويتم تعريفه لكل قيمة فئة مستقرة: $ pure: T \ to M [T] $. لن يكون لقيم $ M [T] $ الناتجة أي آثار جانبية ، لذلك تسمى هذه الوظيفة $ pure $. على سبيل المثال ، بالنسبة لـ I / O monad ، فإن $ pure $ سيعيد بعض القيمة على الفور مع عدم وجود احتمال للفشل.
- الثانية تسمى $ constructor $ ، وبخلاف $ pure $ ، تُرجع $ M [T] $ مع بعض الآثار الجانبية. مثال على هذا $ المُنشئ $ لـ I / O monad غير المتزامن يمكن أن يكون دالة تجلب بعض البيانات من الويب وتعيدها
String
. القيمة التي تم إرجاعها بواسطة $ constructor $ سيكون لها النوع $ M [String] $ في هذه الحالة.
الآن بعد أن أصبح لدينا طريقتان للترويج للقيم في $ M [A] $ ، الأمر متروك لك كمبرمج لاختيار الوظيفة التي تريد استخدامها ، اعتمادًا على أهداف برنامجك. دعنا نفكر في مثال هنا: أنت تريد جلب صفحة HTML مثل https://www.toptal.com/javascript/option-maybe-either-future-monads-js ولهذا عليك إنشاء دالة $ fetch $. نظرًا لإمكانية حدوث أي خطأ أثناء جلبه - فكر في أعطال الشبكة ، وما إلى ذلك - ستستخدم $ M [String] $ كنوع إرجاع لهذه الوظيفة. لذلك سيبدو مثل $ fetch: String \ to M [String] $ وفي مكان ما في جسم الوظيفة هناك سنستخدم $ constructor $ لـ $ M $.
لنفترض الآن أننا نصنع وظيفة وهمية للاختبار: $ fetchMock: String \ to M [String] $. لا يزال يحمل نفس التوقيع ، ولكن هذه المرة نقوم فقط بحقن صفحة HTML الناتجة داخل نص $ fetchMock $ دون القيام بأي عمليات شبكة غير مستقرة. لذا في هذه الحالة نستخدم $ pure $ في تنفيذ $ fetchMock $.
كخطوة تالية ، نحتاج إلى دالة تروج بأمان لأي دالة عشوائية $ f $ من الفئة $ A $ إلى $ M [A] $ (الأسهم الزرقاء في الرسم التخطيطي). تسمى هذه الوظيفة $ map: (T \ to U) \ to (M [T] \ to M [U]) $.
الآن لدينا فئة (يمكن أن يكون لها آثار جانبية إذا استخدمنا $ constructor $) ، والتي تحتوي أيضًا على جميع الوظائف من الفئة المستقرة ، مما يعني أنها مستقرة في $ M [A] $ أيضًا. قد تلاحظ أننا قدمنا صراحة فئة أخرى من الوظائف مثل $ f: T \ to M [U] $. على سبيل المثال ، $ pure $ و $ constructor $ أمثلة على هذه الدوال لـ $ U = T $ ، ولكن من الواضح أنه قد يكون هناك المزيد ، مثل استخدام $ pure $ ثم $ map $. لذلك ، بشكل عام ، نحتاج إلى طريقة للتعامل مع الدوال التعسفية بالصيغة $ f: T \ to M [U] $.
إذا أردنا إنشاء دالة جديدة تعتمد على $ f $ والتي يمكن تطبيقها على $ M [T] $ ، فيمكننا محاولة استخدام $ map $. لكن هذا سيجعلنا نعمل على $ g: M [T] \ to M [M [U]] $ ، وهو أمر غير جيد لأننا لا نريد أن يكون لدينا فئة أخرى $ M [M [A]] $. للتعامل مع هذه المشكلة ، نقدم وظيفة أخيرة: $ flatMap: (T \ to M [U]) \ to (M [T] \ to M [U]) $.
لكن لماذا نريد أن نفعل ذلك؟ لنفترض أننا بعد الخطوة 2 ، على سبيل المثال ، لدينا $ pure $ و $ constructor $ و $ map $. لنفترض أننا نريد الحصول على صفحة HTML من toptal.com ، ثم فحص جميع عناوين URL هناك وكذلك جلبها. سأقوم بعمل دالة fetch: String \ to M [String] $ تجلب عنوان URL واحدًا وتعيد صفحة HTML.
ثم سأطبق هذه الوظيفة على عنوان URL وأحصل على صفحة من toptal.com ، وهي $ x: M [String] $. الآن ، أقوم ببعض التحولات على $ x $ ووصلت أخيرًا إلى بعض عناوين URL $ u: M [String] $. أريد تطبيق الوظيفة $ fetch $ عليها ، لكن لا يمكنني ذلك ، لأنها تتطلب النوع $ String $ ، وليس $ M [String] $. لهذا السبب نحتاج إلى $ flatMap $ لتحويل $ fetch: String \ to M [String] $ إلى $ m_fetch: M [String] \ to M [String] $.
الآن وقد أكملنا جميع الخطوات الثلاث ، يمكننا في الواقع تكوين أي تحويلات قيمة نحتاجها. على سبيل المثال ، إذا كانت لديك قيمة $ x $ من النوع $ M [T] $ و $ f: T \ to U $ ، يمكنك استخدام $ map $ لتطبيق $ f $ على القيمة $ x $ والحصول على القيمة $ y $ من النوع $ M [U] $. بهذه الطريقة يمكن إجراء أي تحويل للقيم بطريقة خالية من الأخطاء بنسبة 100٪ ، طالما أن تطبيقات $ pure $ و $ constructor $ و $ map $ و $ flatMap $ خالية من الأخطاء.
لذا بدلاً من التعامل مع بعض التأثيرات السيئة في كل مرة تصادفها في قاعدة التعليمات البرمجية الخاصة بك ، تحتاج فقط إلى التأكد من تنفيذ هذه الوظائف الأربع فقط بشكل صحيح. في نهاية البرنامج ، ستحصل على $ M [X] $ واحد فقط حيث يمكنك فك القيمة $ X $ بأمان والتعامل مع جميع حالات الخطأ.
هذا ما هو monad: الشيء الذي يطبق $ pure $ و $ map $ و $ flatMap $. (في الواقع يمكن اشتقاق $ map $ من $ pure $ و $ flatMap $ ، لكنها وظيفة مفيدة جدًا وواسعة الانتشار ، لذلك لم أحذفها من التعريف.)
الخيار Monad ، المعروف أيضًا باسم ربما Monad
حسنًا ، دعنا نتعمق في التطبيق العملي واستخدام monads. أول monad مفيد حقًا هو Option monad. إذا كنت قادمًا من لغات البرمجة الكلاسيكية ، فمن المحتمل أنك واجهت العديد من الأعطال بسبب خطأ المؤشر الفارغ السيئ السمعة. يسمي توني هور ، مخترع null ، هذا الاختراع بـ "خطأ المليار دولار":
وقد أدى ذلك إلى عدد لا يحصى من الأخطاء ونقاط الضعف وتعطل النظام ، والتي ربما تسببت في مليار دولار من الألم والأضرار في الأربعين سنة الماضية.
لذلك دعونا نحاول تحسين ذلك. يحتوي الخيار أحادي إما على بعض القيمة غير الفارغة ، أو لا يحتوي على قيمة. تشبه إلى حد كبير القيمة الفارغة ، ولكن بوجود هذا monad ، يمكننا استخدام وظائفنا المحددة جيدًا بأمان دون الخوف من استثناء المؤشر الفارغ. دعنا نلقي نظرة على عمليات التنفيذ بلغات مختلفة:
جافا سكريبت — خيار موناد / ربما موناد
class Monad { // pure :: a -> M a pure = () => { throw "pure method needs to be implemented" } // flatMap :: # M a -> (a -> M b) -> M b flatMap = (x) => { throw "flatMap method needs to be implemented" } // map :: # M a -> (a -> b) -> M b map = f => this.flatMap(x => new this.pure(f(x))) } export class Option extends Monad { // pure :: a -> Option a pure = (value) => { if ((value === null) || (value === undefined)) { return none; } return new Some(value) } // flatMap :: # Option a -> (a -> Option b) -> Option b flatMap = f => this.constructor.name === 'None' ? none : f(this.value) // equals :: # M a -> M a -> boolean equals = (x) => this.toString() === x.toString() } class None extends Option { toString() { return 'None'; } } // Cached None class value export const none = new None() Option.pure = none.pure export class Some extends Option { constructor(value) { super(); this.value = value; } toString() { return `Some(${this.value})` } }
بايثون - أوبشن موناد / ربما موناد
class Monad: # pure :: a -> M a @staticmethod def pure(x): raise Exception("pure method needs to be implemented") # flat_map :: # M a -> (a -> M b) -> M b def flat_map(self, f): raise Exception("flat_map method needs to be implemented") # map :: # M a -> (a -> b) -> M b def map(self, f): return self.flat_map(lambda x: self.pure(f(x))) class Option(Monad): # pure :: a -> Option a @staticmethod def pure(x): return Some(x) # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(self, f): if self.defined: return f(self.value) else: return nil class Some(Option): def __init__(self, value): self.value = value self.defined = True class Nil(Option): def __init__(self): self.value = None self.defined = False nil = Nil()
روبي - أوبشن موناد / ربما موناد
class Monad # pure :: a -> M a def self.pure(x) raise StandardError("pure method needs to be implemented") end # pure :: a -> M a def pure(x) self.class.pure(x) end def flat_map(f) raise StandardError("flat_map method needs to be implemented") end # map :: # M a -> (a -> b) -> M b def map(f) flat_map(-> (x) { pure(f.call(x)) }) end end class Option < Monad attr_accessor :defined, :value # pure :: a -> Option a def self.pure(x) Some.new(x) end # pure :: a -> Option a def pure(x) Some.new(x) end # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(f) if defined f.call(value) else $none end end end class Some < Option def initialize(value) @value = value @defined = true end end class None < Option def initialize() @defined = false end end $none = None.new()
سويفت - خيار موناد / ربما موناد
import Foundation enum Maybe<A> { case None case Some(A) static func pure<B>(_ value: B) -> Maybe<B> { return .Some(value) } func flatMap<B>(_ f: (A) -> Maybe<B>) -> Maybe<B> { switch self { case .None: return .None case .Some(let value): return f(value) } } func map<B>(f: (A) -> B) -> Maybe<B> { return self.flatMap { type(of: self).pure(f($0)) } } }
سكالا - خيار موناد / ربما موناد
import language.higherKinds trait Monad[M[_]] { def pure[A](a: A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(x => pure(f(x))) } object Monad { def apply[F[_]](implicit M: Monad[F]): Monad[F] = M implicit val myOptionMonad = new Monad[MyOption] { def pure[A](a: A) = MySome(a) def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match { case MyNone => MyNone case MySome(a) => f(a) } } } sealed trait MyOption[+A] { def flatMap[B](f: A => MyOption[B]): MyOption[B] = Monad[MyOption].flatMap(this)(f) def map[B](f: A => B): MyOption[B] = Monad[MyOption].map(this)(f) } case object MyNone extends MyOption[Nothing] case class MySome[A](x: A) extends MyOption[A]
نبدأ بتطبيق فئة Monad
التي ستكون الأساس لجميع تطبيقاتنا الأحادية. يعد الحصول على هذه الفئة أمرًا مفيدًا للغاية ، لأنه من خلال تنفيذ طريقتين فقط من أساليبها - pure
وخريطة مسطحة - لوحدة واحدة معينة ، ستحصل على العديد من الطرق مجانًا ( flatMap
على طريقة map
في أمثلةنا ، ولكن بشكل عام هناك العديد من الطرق طرق أخرى مفيدة ، مثل sequence
traverse
للعمل مع مصفوفات من Monad
s).
يمكننا التعبير عن map
كتكوين لخريطة pure
flatMap
. يمكنك أن ترى من توقيع flatMap
$ FlatMap: (T \ to M [U]) \ to (M [T] \ to M [U]) $ أنه قريب جدًا من $ map: (T \ to U) \ إلى (M [T] \ إلى M [U]) $. الفرق هو $ M $ الإضافي في المنتصف ، لكن يمكننا استخدام الدالة pure
لتحويل $ U $ إلى $ M [U] $. بهذه الطريقة نعبر عن map
من حيث خريطة flatMap
pure
.
يعمل هذا بشكل جيد مع Scala ، لأنه يحتوي على نظام كتابة متقدم. كما أنه يعمل بشكل جيد مع JS و Python و Ruby ، لأنه يتم كتابتها ديناميكيًا. لسوء الحظ ، لا يعمل مع Swift ، لأنه مكتوب بشكل ثابت ولا يحتوي على ميزات كتابة متقدمة مثل الأنواع ذات النوع الأعلى ، لذلك بالنسبة لـ Swift ، سيتعين علينا تنفيذ map
لكل monad.
لاحظ أيضًا أن Option monad هو بالفعل معيار واقعي للغات مثل Swift و Scala ، لذلك نستخدم أسماء مختلفة قليلاً لتطبيقات monad الخاصة بنا.
الآن بعد أن أصبح لدينا فئة Monad
أساسية ، دعنا ننتقل إلى تطبيقات Option monad. كما ذكرنا سابقًا ، فإن الفكرة الأساسية هي أن الخيار إما يحمل بعض القيمة (يسمى Some
) أو لا يحمل أي قيمة على الإطلاق ( None
).
تقوم الطريقة pure
ببساطة بترقية قيمة إلى Some
، بينما تتحقق طريقة flatMap
من القيمة الحالية Option
- إذا كانت None
، فإنها تُرجع None
، وإذا كانت Some
ذات قيمة أساسية ، فإنها تستخرج القيمة الأساسية ، وتطبق f()
على وإرجاع نتيجة.
لاحظ أنه بمجرد استخدام هاتين الوظيفتين map
، من المستحيل الدخول في استثناء مؤشر فارغ — على الإطلاق. (من المحتمل أن تظهر المشكلة في تنفيذنا لطريقة flatMap
، ولكن هذا مجرد سطرين في الكود الذي نتحقق منه مرة واحدة. بعد ذلك ، نستخدم فقط تنفيذ الخيار الأحادي عبر التعليمات البرمجية الخاصة بنا في آلاف الأماكن ولا يجب أن تخاف من استثناء المؤشر الفارغ على الإطلاق.)
إما موناد
دعونا نتعمق في الأمر الثاني: إما. هذا هو في الأساس نفس الخيار أحادي ، ولكن مع Some
يسمى Right
ولا None
يسمى Left
. لكن هذه المرة ، يُسمح أيضًا أن يكون Left
قيمة أساسية.
نحن بحاجة إلى ذلك لأنه من الملائم جدًا التعبير عن طرح استثناء. إذا حدث استثناء ، فستكون قيمة Either
Left(Exception)
. لا تتقدم وظيفة flatMap
إذا كانت القيمة على Left
، مما يكرر دلالات طرح الاستثناءات: إذا حدث استثناء ، فإننا نتوقف عن التنفيذ الإضافي.
جافا سكريبت — إما Monad
import Monad from './monad'; export class Either extends Monad { // pure :: a -> Either a pure = (value) => { return new Right(value) } // flatMap :: # Either a -> (a -> Either b) -> Either b flatMap = f => this.isLeft() ? this : f(this.value) isLeft = () => this.constructor.name === 'Left' } export class Left extends Either { constructor(value) { super(); this.value = value; } toString() { return `Left(${this.value})` } } export class Right extends Either { constructor(value) { super(); this.value = value; } toString() { return `Right(${this.value})` } } // attempt :: (() -> a) -> M a Either.attempt = f => { try { return new Right(f()) } catch(e) { return new Left(e) } } Either.pure = (new Left(null)).pure
Python - إما Monad
from monad import Monad class Either(Monad): # pure :: a -> Either a @staticmethod def pure(value): return Right(value) # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(self, f): if self.is_left: return self else: return f(self.value) class Left(Either): def __init__(self, value): self.value = value self.is_left = True class Right(Either): def __init__(self, value): self.value = value self.is_left = False
روبي - إما موناد
require_relative './monad' class Either < Monad attr_accessor :is_left, :value # pure :: a -> Either a def self.pure(value) Right.new(value) end # pure :: a -> Either a def pure(value) self.class.pure(value) end # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(f) if is_left self else f.call(value) end end end class Left < Either def initialize(value) @value = value @is_left = true end end class Right < Either def initialize(value) @value = value @is_left = false end end
سويفت - إما موناد
import Foundation enum Either<A, B> { case Left(A) case Right(B) static func pure<C>(_ value: C) -> Either<A, C> { return Either<A, C>.Right(value) } func flatMap<D>(_ f: (B) -> Either<A, D>) -> Either<A, D> { switch self { case .Left(let x): return Either<A, D>.Left(x) case .Right(let x): return f(x) } } func map<C>(f: (B) -> C) -> Either<A, C> { return self.flatMap { Either<A, C>.pure(f($0)) } } }
سكالا - إما موناد
package monad sealed trait MyEither[+E, +A] { def flatMap[EE >: E, B](f: A => MyEither[EE, B]): MyEither[EE, B] = Monad[MyEither[EE, ?]].flatMap(this)(f) def map[EE >: E, B](f: A => B): MyEither[EE, B] = Monad[MyEither[EE, ?]].map(this)(f) } case class MyLeft[E](e: E) extends MyEither[E, Nothing] case class MyRight[A](a: A) extends MyEither[Nothing, A] // ... implicit def myEitherMonad[E] = new Monad[MyEither[E, ?]] { def pure[A](a: A) = MyRight(a) def flatMap[A, B](ma: MyEither[E, A])(f: A => MyEither[E, B]): MyEither[E, B] = ma match { case MyLeft(a) => MyLeft(a) case MyRight(b) => f(b) } }
لاحظ أيضًا أنه من السهل التقاط الاستثناءات: كل ما عليك فعله هو تعيين Left
إلى Right
. (على الرغم من أننا لا نفعل ذلك في الأمثلة لدينا ، للإيجاز.)

موناد المستقبل
دعنا نستكشف آخر monad نحتاجه: المستقبل monad. المستقبل الأحادي هو في الأساس حاوية للقيمة التي إما أن تكون متاحة الآن أو ستكون متاحة في المستقبل القريب. يمكنك إنشاء سلاسل من Futures باستخدام map
و flatMap
والتي ستنتظر حل القيمة المستقبلية قبل تنفيذ الجزء التالي من التعليمات البرمجية الذي يعتمد على القيمة التي يتم حلها أولاً. هذا مشابه جدًا لمفهوم الوعود في JS.
هدفنا التصميمي الآن هو ربط واجهات برمجة التطبيقات غير المتزامنة الحالية بلغات مختلفة بقاعدة واحدة متسقة. اتضح أن أسهل نهج تصميمي هو استخدام عمليات الاسترجاعات في $ constructor $.
في حين أن تصميم رد الاتصال قدم مشكلة رد الاتصال في JavaScript ولغات أخرى ، فلن يمثل ذلك مشكلة بالنسبة لنا ، نظرًا لأننا نستخدم monads. في الواقع ، كائن Promise
- أساس حل JavaScript لجحيم رد النداء - هو monad بحد ذاته!
ماذا عن منشئ المستقبل الأحادي؟ هل لديه هذا التوقيع:
constructor :: ((Either err a -> void) -> void) -> Future (Either err a)
دعونا نقسمها إلى أجزاء. أولاً ، دعنا نحدد:
type Callback = Either err a -> void
لذا فإن Callback
الاتصال هو وظيفة تأخذ إما خطأ أو قيمة تم حلها كوسيطة ، ولا تُرجع شيئًا. يبدو توقيعنا الآن كما يلي:
constructor :: (Callback -> void) -> Future (Either err a)
لذلك نحن بحاجة إلى توفير وظيفة لا تُرجع شيئًا وتؤدي إلى إعادة الاتصال بمجرد أن يتم حل الحساب غير المتزامن لخطأ أو إلى قيمة ما. تبدو سهلة بما يكفي لإنشاء جسر لأي لغة.
بالنسبة لتصميم المستقبل الأحادي نفسه ، فلنلقِ نظرة على هيكله الداخلي. الفكرة الأساسية هي أن يكون لديك متغير ذاكرة تخزين مؤقت يحمل قيمة إذا تم حل monad المستقبل ، أو لا يحمل أي شيء بخلاف ذلك. يمكنك الاشتراك في Future بمعاودة الاتصال والتي سيتم تشغيلها على الفور إذا تم حل القيمة ، أو إذا لم يتم حلها ، فسيتم وضع رد الاتصال في قائمة المشتركين.
بمجرد حل المستقبل ، سيتم تشغيل كل رد اتصال في هذه القائمة مرة واحدة بالضبط مع القيمة التي تم حلها في سلسلة منفصلة (أو كوظيفة تالية يتم تنفيذها في حلقة الحدث ، في حالة JS.) لاحظ أنه من المهم أن استخدم بدائل التزامن بعناية ، وإلا فمن الممكن أن تكون ظروف السباق ممكنة.
التدفق الأساسي هو: تبدأ الحساب غير المتزامن المقدم كوسيطة منشئ ، وتوجه رد الاتصال الخاص به إلى طريقة رد الاتصال الداخلية الخاصة بنا. في غضون ذلك ، يمكنك الاشتراك في Future monad ووضع استجاباتك في قائمة الانتظار. بمجرد الانتهاء من الحساب ، تستدعي طريقة رد النداء الداخلي جميع عمليات الاسترجاعات في قائمة الانتظار. إذا كنت معتادًا على الامتدادات التفاعلية (RxJS ، RxSwift ، إلخ) ، فإنها تستخدم أسلوبًا مشابهًا جدًا للتعامل غير المتزامن.
تتكون API العامة لمستقبل monad من خريطة pure
map
مسطحة ، تمامًا كما هو الحال في flatMap
السابقة. سنحتاج أيضًا إلى طريقتين مفيدتين:
- غير
async
، الذي يأخذ وظيفة حظر متزامن وينفذها على مؤشر ترابط منفصل ، و -
traverse
، الذي يأخذ مصفوفة من القيم والوظيفة التي تعين قيمة إلىFuture
، وتعيدFuture
مصفوفة من القيم التي تم حلها
دعونا نرى كيف يتم ذلك:
جافا سكريبت — فيوتشر موناد
import Monad from './monad'; import { Either, Left, Right } from './either'; import { none, Some } from './option'; export class Future extends Monad { // constructor :: ((Either err a -> void) -> void) -> Future (Either err a) constructor(f) { super(); this.subscribers = []; this.cache = none; f(this.callback) } // callback :: Either err a -> void callback = (value) => { this.cache = new Some(value) while (this.subscribers.length) { const subscriber = this.subscribers.shift(); subscriber(value) } } // subscribe :: (Either err a -> void) -> void subscribe = (subscriber) => (this.cache === none ? this.subscribers.push(subscriber) : subscriber(this.cache.value)) toPromise = () => new Promise( (resolve, reject) => this.subscribe(val => val.isLeft() ? reject(val.value) : resolve(val.value)) ) // pure :: a -> Future a pure = Future.pure // flatMap :: (a -> Future b) -> Future b flatMap = f => new Future( cb => this.subscribe(value => value.isLeft() ? cb(value) : f(value.value).subscribe(cb)) ) } Future.async = (nodeFunction, ...args) => { return new Future(cb => nodeFunction(...args, (err, data) => err ? cb(new Left(err)) : cb(new Right(data))) ); } Future.pure = value => new Future(cb => cb(Either.pure(value))) // traverse :: [a] -> (a -> Future b) -> Future [b] Future.traverse = list => f => list.reduce( (acc, elem) => acc.flatMap(values => f(elem).map(value => [...values, value])), Future.pure([]) )
بايثون - فيوتشر موناد
from monad import Monad from option import nil, Some from either import Either, Left, Right from functools import reduce import threading class Future(Monad): # __init__ :: ((Either err a -> void) -> void) -> Future (Either err a) def __init__(self, f): self.subscribers = [] self.cache = nil self.semaphore = threading.BoundedSemaphore(1) f(self.callback) # pure :: a -> Future a @staticmethod def pure(value): return Future(lambda cb: cb(Either.pure(value))) def exec(f, cb): try: data = f() cb(Right(data)) except Exception as err: cb(Left(err)) def exec_on_thread(f, cb): t = threading.Thread(target=Future.exec, args=[f, cb]) t.start() def async(f): return Future(lambda cb: Future.exec_on_thread(f, cb)) # flat_map :: (a -> Future b) -> Future b def flat_map(self, f): return Future( lambda cb: self.subscribe( lambda value: cb(value) if (value.is_left) else f(value.value).subscribe(cb) ) ) # traverse :: [a] -> (a -> Future b) -> Future [b] def traverse(arr): return lambda f: reduce( lambda acc, elem: acc.flat_map( lambda values: f(elem).map( lambda value: values + [value] ) ), arr, Future.pure([])) # callback :: Either err a -> void def callback(self, value): self.semaphore.acquire() self.cache = Some(value) while (len(self.subscribers) > 0): sub = self.subscribers.pop(0) t = threading.Thread(target=sub, args=[value]) t.start() self.semaphore.release() # subscribe :: (Either err a -> void) -> void def subscribe(self, subscriber): self.semaphore.acquire() if (self.cache.defined): self.semaphore.release() subscriber(self.cache.value) else: self.subscribers.append(subscriber) self.semaphore.release()
روبي - فيوتشر موناد
require_relative './monad' require_relative './either' require_relative './option' class Future < Monad attr_accessor :subscribers, :cache, :semaphore # initialize :: ((Either err a -> void) -> void) -> Future (Either err a) def initialize(f) @subscribers = [] @cache = $none @semaphore = Queue.new @semaphore.push(nil) f.call(method(:callback)) end # pure :: a -> Future a def self.pure(value) Future.new(-> (cb) { cb.call(Either.pure(value)) }) end def self.async(f, *args) Future.new(-> (cb) { Thread.new { begin cb.call(Right.new(f.call(*args))) rescue => e cb.call(Left.new(e)) end } }) end # pure :: a -> Future a def pure(value) self.class.pure(value) end # flat_map :: (a -> Future b) -> Future b def flat_map(f) Future.new(-> (cb) { subscribe(-> (value) { if (value.is_left) cb.call(value) else f.call(value.value).subscribe(cb) end }) }) end # traverse :: [a] -> (a -> Future b) -> Future [b] def self.traverse(arr, f) arr.reduce(Future.pure([])) do |acc, elem| acc.flat_map(-> (values) { f.call(elem).map(-> (value) { values + [value] }) }) end end # callback :: Either err a -> void def callback(value) semaphore.pop self.cache = Some.new(value) while (subscribers.count > 0) sub = self.subscribers.shift Thread.new { sub.call(value) } end semaphore.push(nil) end # subscribe :: (Either err a -> void) -> void def subscribe(subscriber) semaphore.pop if (self.cache.defined) semaphore.push(nil) subscriber.call(cache.value) else self.subscribers.push(subscriber) semaphore.push(nil) end end end
سويفت - فيوتشر موناد
import Foundation let background = DispatchQueue(label: "background", attributes: .concurrent) class Future<Err, A> { typealias Callback = (Either<Err, A>) -> Void var subscribers: Array<Callback> = Array<Callback>() var cache: Maybe<Either<Err, A>> = .None var semaphore = DispatchSemaphore(value: 1) lazy var callback: Callback = { value in self.semaphore.wait() self.cache = .Some(value) while (self.subscribers.count > 0) { let subscriber = self.subscribers.popLast() background.async { subscriber?(value) } } self.semaphore.signal() } init(_ f: @escaping (@escaping Callback) -> Void) { f(self.callback) } func subscribe(_ cb: @escaping Callback) { self.semaphore.wait() switch cache { case .None: subscribers.append(cb) self.semaphore.signal() case .Some(let value): self.semaphore.signal() cb(value) } } static func pure<B>(_ value: B) -> Future<Err, B> { return Future<Err, B> { $0(Either<Err, B>.pure(value)) } } func flatMap<B>(_ f: @escaping (A) -> Future<Err, B>) -> Future<Err, B> { return Future<Err, B> { [weak self] cb in guard let this = self else { return } this.subscribe { value in switch value { case .Left(let err): cb(Either<Err, B>.Left(err)) case .Right(let x): f(x).subscribe(cb) } } } } func map<B>(_ f: @escaping (A) -> B) -> Future<Err, B> { return self.flatMap { Future<Err, B>.pure(f($0)) } } static func traverse<B>(_ list: Array<A>, _ f: @escaping (A) -> Future<Err, B>) -> Future<Err, Array<B>> { return list.reduce(Future<Err, Array<B>>.pure(Array<B>())) { (acc: Future<Err, Array<B>>, elem: A) in return acc.flatMap { elems in return f(elem).map { val in return elems + [val] } } } } }
سكالا - فيوتشر موناد
package monad import java.util.concurrent.Semaphore class MyFuture[A] { private var subscribers: List[MyEither[Exception, A] => Unit] = List() private var cache: MyOption[MyEither[Exception, A]] = MyNone private val semaphore = new Semaphore(1) def this(f: (MyEither[Exception, A] => Unit) => Unit) { this() f(this.callback _) } def flatMap[B](f: A => MyFuture[B]): MyFuture[B] = Monad[MyFuture].flatMap(this)(f) def map[B](f: A => B): MyFuture[B] = Monad[MyFuture].map(this)(f) def callback(value: MyEither[Exception, A]): Unit = { semaphore.acquire cache = MySome(value) subscribers.foreach { sub => val t = new Thread( new Runnable { def run: Unit = { sub(value) } } ) t.start } subscribers = List() semaphore.release } def subscribe(sub: MyEither[Exception, A] => Unit): Unit = { semaphore.acquire cache match { case MyNone => subscribers = sub :: subscribers semaphore.release case MySome(value) => semaphore.release sub(value) } } } object MyFuture { def async[B, C](f: B => C, arg: B): MyFuture[C] = new MyFuture[C]({ cb => val t = new Thread( new Runnable { def run: Unit = { try { cb(MyRight(f(arg))) } catch { case e: Exception => cb(MyLeft(e)) } } } ) t.start }) def traverse[A, B](list: List[A])(f: A => MyFuture[B]): MyFuture[List[B]] = { list.foldRight(Monad[MyFuture].pure(List[B]())) { (elem, acc) => Monad[MyFuture].flatMap(acc) ({ values => Monad[MyFuture].map(f(elem)) { value => value :: values } }) } } } // ... implicit val myFutureMonad = new Monad[MyFuture] { def pure[A](a: A): MyFuture[A] = new MyFuture[A]({ cb => cb(myEitherMonad[Exception].pure(a)) }) def flatMap[A, B](ma: MyFuture[A])(f: A => MyFuture[B]): MyFuture[B] = new MyFuture[B]({ cb => ma.subscribe(_ match { case MyLeft(e) => cb(MyLeft(e)) case MyRight(a) => f(a).subscribe(cb) }) }) }
الآن ، لاحظ كيف أن واجهة برمجة التطبيقات العامة لـ Future
لا تحتوي على أي تفاصيل منخفضة المستوى مثل الخيوط أو الإشارات أو أي من تلك الأشياء. كل ما تحتاجه في الأساس هو تزويد شيء ما بمعاودة الاتصال ، وهذا كل شيء!
يؤلف برنامج من Monads
حسنًا ، دعونا نحاول استخدام monads لدينا لعمل برنامج فعلي. لنفترض أن لدينا ملفًا يحتوي على قائمة بعناوين URL ونريد جلب كل عنوان من عناوين URL هذه على التوازي. بعد ذلك ، نريد قص الاستجابات إلى 200 بايت لكل منها للإيجاز وطباعة النتيجة.
نبدأ بتحويل واجهات برمجة تطبيقات اللغة الحالية إلى واجهات أحادية (انظر الوظائف readFile
و fetch
). الآن بعد أن حصلنا على ذلك ، يمكننا فقط تكوينها للحصول على النتيجة النهائية كسلسلة واحدة. لاحظ أن السلسلة نفسها آمنة للغاية ، حيث أن جميع التفاصيل الدموية موجودة في monads.
JavaScript - نموذج لبرنامج Monad
import { Future } from './future'; import { Either, Left, Right } from './either'; import { readFile } from 'fs'; import https from 'https'; const getResponse = url => new Future(cb => https.get(url, res => { var body = ''; res.on('data', data => body += data); res.on('end', data => cb(new Right(body))); res.on('error', err => cb(new Left(err))) })) const getShortResponse = url => getResponse(url).map(resp => resp.substring(0, 200)) Future .async(readFile, 'resources/urls.txt') .map(data => data.toString().split("\n")) .flatMap(urls => Future.traverse(urls)(getShortResponse)) .map(console.log)
Python—Sample Monad Program
import http.client import threading import time import os from future import Future from either import Either, Left, Right conn = http.client.HTTPSConnection("en.wikipedia.org") def read_file_sync(uri): base_dir = os.path.dirname(__file__) #<-- absolute dir the script is in path = os.path.join(base_dir, uri) with open(path) as f: return f.read() def fetch_sync(uri): conn.request("GET", uri) r = conn.getresponse() return r.read().decode("utf-8")[:200] def read_file(uri): return Future.async(lambda: read_file_sync(uri)) def fetch(uri): return Future.async(lambda: fetch_sync(uri)) def main(args=None): lines = read_file("../resources/urls.txt").map(lambda res: res.splitlines()) content = lines.flat_map(lambda urls: Future.traverse(urls)(fetch)) output = content.map(lambda res: print("\n".join(res))) if __name__ == "__main__": main()
Ruby—Sample Monad Program
require './lib/future' require 'net/http' require 'uri' semaphore = Queue.new def read(uri) Future.async(-> () { File.read(uri) }) end def fetch(url) Future.async(-> () { uri = URI(url) Net::HTTP.get_response(uri).body[0..200] }) end read("resources/urls.txt") .map(-> (x) { x.split("\n") }) .flat_map(-> (urls) { Future.traverse(urls, -> (url) { fetch(url) }) }) .map(-> (res) { puts res; semaphore.push(true) }) semaphore.pop
Swift—Sample Monad Program
import Foundation enum Err: Error { case Some(String) } func readFile(_ path: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(fileURLWithPath: path) let text = try? String(contentsOf: url) if let res = text { callback(Either<Error, String>.pure(res)) } else { callback(Either<Error, String>.Left(Err.Some("Error reading urls.txt"))) } } } } func fetchUrl(_ url: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(string: url) let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in if let err = error { callback(Either<Error, String>.Left(err)) return } guard let nonEmptyData = data else { callback(Either<Error, String>.Left(Err.Some("Empty response"))) return } guard let result = String(data: nonEmptyData, encoding: String.Encoding.utf8) else { callback(Either<Error, String>.Left(Err.Some("Cannot decode response"))) return } let index = result.index(result.startIndex, offsetBy: 200) callback(Either<Error, String>.pure(String(result[..<index]))) } task.resume() } } } var result: Any = "" let _ = readFile("\(projectDir)/Resources/urls.txt") .map { data -> [String] in data.components(separatedBy: "\n").filter { (line: String) in !line.isEmpty } }.flatMap { urls in return Future<Error, String>.traverse(urls) { url in return fetchUrl(url) } }.map { responses in print(responses) } RunLoop.main.run()
Scala—Sample Monad Program
import scala.io.Source import java.util.concurrent.Semaphore import monad._ object Main extends App { val semaphore = new Semaphore(0) def readFile(name: String): MyFuture[List[String]] = MyFuture.async[String, List[String]](filename => Source.fromResource(filename).getLines.toList, name) def fetch(url: String): MyFuture[String] = MyFuture.async[String, String]( uri => Source.fromURL(uri).mkString.substring(0, 200), url ) val future = for { urls <- readFile("urls.txt") entries <- MyFuture.traverse(urls)(fetch _) } yield { println(entries) semaphore.release } semaphore.acquire }
There you have it—monad solutions in practice. You can find a repo containing all the code from this article on GitHub.
Overhead: Done. Benefits: Ongoing
For this simple monad-based program, it might look like overkill to use all the code that we wrote before. But that's just the initial setup, and it will stay constant in its size. Imagine that from now on, using monads, you can write a lot of async code, not worrying about threads, race conditions, semaphores, exceptions, or null pointers! مدهش!