لماذا يتم تصميم الآثار الجانبية على أنها موناد في هاسكل؟

StackOverflow https://stackoverflow.com/questions/2488646

سؤال

هل يمكن لأي شخص أن يعطي بعض المؤشرات حول سبب صياغة الحسابات النجمية في هاسكل على أنها موناد؟

أعني أن موناد هو مجرد واجهة مع 4 عمليات ، فماذا كان سبب النمذجة الآثار الجانبية فيه؟

هل كانت مفيدة؟

المحلول

لنفترض أن الوظيفة لها آثار جانبية. إذا أخذنا جميع التأثيرات التي تنتجها كمعلمات الإدخال والإخراج ، فإن الوظيفة نقية للعالم الخارجي.

لذلك من أجل وظيفة نجس

f' :: Int -> Int

نضيف العالم الحقيقي إلى الاعتبار

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

من ثم f نقي مرة أخرى. نحدد نوع البيانات المعتمدة IO a = RealWorld -> (a, RealWorld), ، لذلك نحن لا نحتاج إلى كتابة RealWorld عدة مرات

f :: Int -> IO Int

بالنسبة للمبرمج ، فإن التعامل مع العالم الحقيقي أمر خطير للغاية - على وجه الخصوص ، إذا كان المبرمج يحصل على قيمة من النوع الحقيقي ، فقد يحاولون ذلك ينسخ هذا ، وهو أمر مستحيل بشكل أساسي. (فكر في محاولة نسخ نظام الملفات بأكمله ، على سبيل المثال. أين تضعه؟) لذلك ، فإن تعريفنا لـ IO يلف حالات العالم بأسره أيضًا.

هذه الوظائف غير المجدية عديمة الفائدة إذا لم نتمكن من ربطها ببعضها البعض. يعتبر

getLine :: IO String               = RealWorld -> (String, RealWorld)
getContents :: String -> IO String = String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO ()        = String -> RealWorld -> ((), RealWorld)

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

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

نرى نمطًا هنا: تسمى الوظائف مثل هذا:

...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

لذلك يمكننا تحديد المشغل ~~~ لربطهم:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b, RealWorld))
      -> (b -> RealWorld -> (c, RealWorld))
      ->       RealWorld -> (c, RealWorld)
(f ~~~ g) worldX = let (resF, worldY) = f worldX in
                        g resF worldY

ثم يمكننا ببساطة الكتابة

printFile = getLine ~~~ getContents ~~~ putStrLn

دون لمس العالم الحقيقي.


لنفترض الآن أننا نريد جعل محتوى الملف كبيرًا أيضًا. العلوي هو وظيفة خالصة

upperCase :: String -> String

ولكن لجعله في العالم الحقيقي ، يجب أن يعيد IO String. من السهل رفع هذه الوظيفة:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

يمكن تعميم هذا:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

لهذا السبب. impureUpperCase = impurify . upperCase, ، ويمكننا الكتابة

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(ملاحظة: عادة ما نكتب getLine ~~~ getContents ~~~ (putStrLn . upperCase))


الآن دعنا نرى ما فعلناه:

  1. حددنا مشغل (~~~) :: IO b -> (b -> IO c) -> IO c الذي يقوم بسلاسل وظيفتين نجس معًا
  2. حددنا وظيفة impurify :: a -> IO a الذي يحول قيمة نقية إلى نجس.

الآن نقوم بتحديد الهوية (>>=) = (~~~) و return = impurify, ، ونرى؟ لدينا موناد.


(للتحقق مما إذا كان هذا هو في الحقيقة موناد ، يجب أن يكون هناك القليل من البديهيات: يجب أن تكون راضية:

(1) return a >>= f = f a

  impurify a               = (\world -> (a, world))
 (impurify a ~~~ f) worldX = let (resF, worldY) = (\world -> (a, world)) worldX 
                             in f resF worldY
                           = let (resF, worldY) =            (a, worldX))       
                             in f resF worldY
                           = f a worldX

(2) f >>= return = f

  (f ~~~ impurify) a worldX = let (resF, worldY) = impuify a worldX 
                              in f resF worldY
                            = let (resF, worldY) = (a, worldX)     
                              in f resF worldY
                            = f a worldX

(3) f >>= (\x -> g x >>= h) = (f >>= g) >>= h

يمارس.)

نصائح أخرى

هل يمكن لأي شخص أن يعطي بعض المؤشرات حول سبب صياغة الحسابات غير الناخبة في هاسكل على أنها موناد؟

يحتوي هذا السؤال على سوء فهم واسع النطاق. الشوائب وموناد مفاهيم مستقلة. الشوائب ليس على غرار موناد. بدلاً من ذلك ، هناك بعض أنواع البيانات ، مثل IO, التي تمثل الحساب الضروري. وبالنسبة لبعض هذه الأنواع ، يتوافق جزء صغير من واجهتهم مع نمط الواجهة المسمى "Monad". علاوة على ذلك ، لا يوجد تفسير نقي/وظيفي/مشير IO (ومن غير المرجح أن يكون هناك واحد ، بالنظر إلى "Sin Bin" الغرض من IO) ، على الرغم من وجود قصة شائعة حول World -> (a, World) كونه معنى IO a. لا يمكن أن تصف تلك القصة بصدق IO, ، لان IO يدعم التزامن و nondeterminism. لا تعمل القصة حتى عندما تكون الحسابات الحتمية التي تسمح بالتفاعل مع العالم مع العالم.

لمزيد من التفسير ، انظر هذا الجواب.

تعديل: عند إعادة قراءة السؤال ، لا أعتقد أن إجابتي على المسار الصحيح. غالبًا ما تتحول نماذج الحساب الضروري إلى موناد ، تمامًا كما قال السؤال. قد لا يفترض Asker حقًا أن الأحاديات بأي شكل من الأشكال تتيح نمذجة الحساب الضروري.

كما أفهمها ، اتصل شخص ما يوجينيو موغجي لاحظت أولاً أنه يمكن استخدام بنية رياضية غامضة مسبقًا تسمى "موناد" لنمذجة الآثار الجانبية بلغات الكمبيوتر ، وبالتالي تحديد دلالاتها باستخدام حساب حساب Lambda. عندما تم تطوير Haskell ، كانت هناك طرق مختلفة تم فيها تصميم الحسابات النجمية (انظر Simon Peyton Jones " ورقة "قميص الشعر" لمزيد من التفاصيل) ، ولكن عندما قدم Phil Wadler Monads ، أصبح من الواضح أن هذا هو الحل. والباقي هو التاريخ.

هل يمكن لأي شخص أن يعطي بعض المؤشرات حول سبب صياغة الحسابات غير الناخبة في هاسكل على أنها موناد؟

حسنًا ، لأن هاسكل نقي. تحتاج إلى مفهوم رياضي للتمييز بين الحسابات غير النقي و نقي تشغيل مستوى النوع ونموذج تدفق البرامج على التوالي.

هذا يعني أنه يجب أن ينتهي بك الأمر إلى نوع ما IO a أن النماذج حساب غير متكافئ. ثم تحتاج إلى معرفة طرق الجمع هذه الحسابات منها تنطبق في التسلسل (>>=) و ارفع قيمة (return) هي الأكثر وضوحا وأساسيا.

مع هذين ، لقد حددت بالفعل موناد (حتى دون التفكير في الأمر) ؛)

بالإضافة إلى ذلك ، يقدم Monads تجريدات عامة وقوية للغاية, ، يمكن تعميم أنواع كثيرة من تدفق التحكم بشكل مناسب في وظائف أحادية مثل sequence, liftM أو بناء الجملة الخاص ، مما يجعل الفكنة ليست مثل هذه الحالة الخاصة.

يرى الموناد في البرمجة الوظيفية و كتابة التفرد (البديل الوحيد الذي أعرفه) لمزيد من المعلومات.

كما تقول، Monad هو بنية بسيطة جدا. نصف الجواب هو: Monad هو أبسط هيكل يمكن أن نعطيه لوظائف التأثير الجانبي وتكون قادرة على استخدامها. مع Monad يمكننا أن نفعل شيئين: يمكننا التعامل مع قيمة خالصة كقيمة ذات تأثير جانبي (return) ، ويمكننا تطبيق وظيفة التأثير الجانبي على قيمة التأثير الجانبي للحصول على قيمة جديدة لاستئجار جانبي (>>=). إن فقدان القدرة على القيام بأي من هذه الأشياء سيكون معطلًا ، لذلك يجب أن يكون نوع التأثير الجانبي "على الأقل" Monad, ، واتضح Monad يكفي لتنفيذ كل ما نحتاجه حتى الآن.

النصف الآخر هو: ما هو الهيكل الأكثر تفصيلاً الذي يمكن أن نقدمه لـ "الآثار الجانبية المحتملة"؟ يمكننا بالتأكيد التفكير في مساحة جميع الآثار الجانبية الممكنة كمجموعة (العملية الوحيدة التي تتطلب العضوية). يمكننا الجمع بين آثار جانبية من خلال القيام بهما واحدًا تلو الآخر ، وسيؤدي ذلك إلى ظهور تأثير جانبي مختلف (أو ربما هو نفسه - إذا كان الأول هو "الكمبيوتر الإغلاق" والثاني هو "ملف الكتابة" ، ثم النتيجة من تأليف هذه هو مجرد "Cutdown Computer").

حسنًا ، فماذا يمكن أن نقول عن هذه العملية؟ إنه ترابط. هذا هو ، إذا جمعنا بين ثلاث آثار جانبية ، فلا يهم أي الطلب الذي نقوم به في الجمع الحاسوب). لكنها ليست مبتدئة: ("ملف الكتابة" ثم "حذف الملف") هو تأثير جانبي مختلف من ("حذف الملف" ثم "ملف الكتابة"). ولدينا هوية: الآثار الجانبية الخاصة "لا توجد آثار جانبية" تعمل ("لا توجد آثار جانبية" ثم "حذف الملف" هو نفس التأثير الجانبي مثل "حذف الملف") في هذه المرحلة أي عالم رياضيات يفكر فيه "مجموعة!" لكن المجموعات لديها عكسيات ، وليس هناك طريقة لعكس الآثار الجانبية بشكل عام ؛ "حذف الملف" لا رجعة فيه. لذا فإن الهيكل الذي تركناه هو البنية أحادية ، مما يعني أن وظائف التأثير الجانبية لدينا يجب أن تكون أحاديات.

هل يوجد بنية أكثر تعقيدًا؟ بالتأكيد! يمكننا تقسيم الآثار الجانبية المحتملة إلى تأثيرات قائمة على نظام الملفات والتأثيرات القائمة على الشبكة والمزيد ، ويمكننا التوصل إلى قواعد تكوين أكثر تفصيلاً والتي حافظت على هذه التفاصيل. ولكن مرة أخرى يتعلق الأمر بـ: Monad بسيط للغاية ، ولكنه قوي بما يكفي للتعبير عن معظم الخصائص التي نهتم بها. (على وجه الخصوص ، دعونا نرتبط والبديهيات الأخرى لاختبار تطبيقنا في قطع صغيرة ، بثقة في أن الآثار الجانبية للتطبيق المشترك ستكون هي نفس مزيج الآثار الجانبية للقطع).

إنها في الواقع طريقة نظيفة للتفكير في I/O بطريقة وظيفية.

في معظم لغات البرمجة ، يمكنك إجراء عمليات الإدخال/الإخراج. في هاسكل ، تخيل عدم كتابة رمز فعل العمليات ، ولكن لإنشاء قائمة بالعمليات التي ترغب في القيام بها.

Monads هي مجرد بناء جملة لذلك بالضبط.

إذا كنت تريد أن تعرف لماذا Monads بدلاً من شيء آخر ، أعتقد أن الإجابة هي أنها أفضل طريقة وظيفية لتمثيل I/O يمكن للناس التفكير فيها عندما كانوا يصنعون Haskell.

AFAIK ، والسبب هو أن تكون قادرًا على تضمين عمليات فحص الآثار الجانبية في نظام النوع. إذا كنت تريد معرفة المزيد ، استمع إلى هؤلاء Se-Radio الحلقات: الحلقة 108: سيمون بيتون جونز على البرمجة الوظيفية وحل الحلقة 72: Erik Meijer on Linq

أعلاه هناك إجابات مفصلة جيدة للغاية مع خلفية نظرية. لكني أريد أن أعطي وجهة نظري على io monad. أنا لست من ذوي الخبرة في برنامج Haskell ، لذلك قد يكون ساذجًا جدًا أو حتى خطأ. لكنني ساعدتني في التعامل مع IO Monad إلى حد ما (ملاحظة ، أنه لا يتعلق بالموناد الآخرين).

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

لذلك نريد أن تكون لغتنا (Haskell) نقية. لكننا نحتاج إلى عمليات الإدخال/الإخراج كما لو أن برنامجنا لا يمكن أن يكون مفيدًا. ولا يمكن أن تكون تلك العمليات نقية بطبيعتها. لذا فإن الطريقة الوحيدة للتعامل مع هذا ، علينا فصل العمليات النجمية عن بقية التعليمات البرمجية.

هنا يأتي موناد. في الواقع ، لست متأكدًا من أنه لا يمكن أن يكون هناك بناء آخر ذو خصائص متشابهة مطلوبة ، ولكن النقطة هي أن Monad لها هذه الخصائص ، بحيث يمكن استخدامها (ويتم استخدامها بنجاح). الخاصية الرئيسية هي أننا لا نستطيع الهروب منها. لا تملك واجهة Monad عمليات للتخلص من الموناد حول قيمتنا. يوفر أحاديات (ليس IO) الآخرون مثل هذه العمليات ويسمحون بمطابقة الأنماط (على سبيل المثال ربما) ، ولكن هذه العمليات ليست في واجهة Monad. خاصية أخرى مطلوبة هي القدرة على سلسلة العمليات.

إذا فكرنا في ما نحتاجه من حيث نظام النوع ، فإننا نصل إلى حقيقة أننا نحتاج إلى النوع مع مُنشئ ، والذي يمكن لفه حول أي Vale. يجب أن يكون المنشئ خاصًا ، حيث نحظر الهروب منه (أي مطابقة للنمط). لكننا بحاجة إلى وظيفة لوضع القيمة في هذا المُنشئ (هنا يتبادر إلى الذهن). ونحن بحاجة إلى طريقة لسلسلة العمليات. إذا فكرنا في الأمر لبعض الوقت ، فسوف نصل إلى الحقيقة ، يجب أن يكون لعملية التسلسل نوعًا ما >> = لديه. لذلك ، وصلنا إلى شيء مشابه جدًا لموناد. أعتقد ، إذا قمنا الآن بتحليل المواقف المتناقضة المحتملة مع هذا البناء ، فسوف نأتي إلى البديهيات Monad.

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

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

أخيرًا ، أود أن أقول ، أن موناد لا يتحول العمليات النقي إلى عمليات نقية. يسمح فقط بفصلها بفعالية. (أكرر ، أن فهمي فقط)

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top