سؤال

أنا مؤخرا طرح سؤالا حول البرمجة الوظيفية، وحصلوا على إجابات (جيدة!) أثارت المزيد من الأسئلة (كما يبدو أن هذا هو الحال مع التعلم، في بعض الأحيان).وهنا بضعة أمثلة:

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

  2. هذه الإجابة تحدثت عن الحالة التي يتم فيها تنفيذ العمليات في حلقة على كائن، وكيف يمكنك استخدام كائن جديد في كل مرة بدلاً من تحديث كائن قديم.ومع ذلك، دعنا نقول bankAccount يتم تحديثه في سيناريو غير متكرر - على سبيل المثال، النظام المصرفي لواجهة المستخدم الرسومية.ينقر عامل التشغيل على الزر "تغيير سعر الفائدة"، الذي يطلق حدثًا من شأنه (في C# على سبيل المثال) أن يفعل شيئًا مثل bankAccount.InterestRate = newRateFromUser.أشعر وكأنني كثيف هنا، ولكن آمل أن يكون مثالي منطقيًا:يجب أن تكون هناك طريقة ما لتحديث الكائن، أليس كذلك؟قد تعتمد عدة أشياء أخرى على البيانات الجديدة.

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

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

المحلول

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

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

نصائح أخرى

فكر في فئة السلسلة في .Net (وهو كائن غير قابل للتغيير).إذا قمت باستدعاء عملية على سلسلة، فستحصل على نسخة جديدة:

String s1 = "there";
String s2 = s1.Insert(0, "hello ");

Console.Writeline("string 1: " + s1);
Console.Writeline("string 2: " + s2);

سيؤدي هذا إلى إخراج:

السلسلة 1:هناك

السلسلة 2:أهلاً بك

قارن هذا السلوك بـ StringBuilder، الذي له نفس توقيع الطريقة بشكل أساسي:

StringBuilder sb  = new StringBuilder("there");
StringBuilder sb2 = sb.Insert(0, "hi ");

Console.WriteLine("sb 1: " + sb.ToString());
Console.WriteLine("sb 2: " + sb2.ToString());

نظرًا لأن StringBuilder قابل للتغيير، يشير كلا المتغيرين إلى نفس الكائن.سيكون الإخراج:

بينالي الشارقة 1:أهلاً

بينالي الشارقة 2:أهلاً

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

بالنسبه للرقم 2...

قد تعتمد العديد من الأشياء الأخرى على البيانات الجديدة.

وهذا ما يسميه الأصوليون "التأثير".إن فكرة إشارات الكائنات المتعددة إلى نفس الكائن القابل للتغيير هي جوهر الحالة القابلة للتغيير وجوهر المشكلة.في OOP، قد يكون لديك كائن "a" من النوع BankAccount، وإذا قرأت a.Balance أو غير ذلك في مكان مختلف مرات قد ترى قيمًا مختلفة.في المقابل، في FP الخالص، إذا كان "a" يحتوي على نوع BankAccount، فهو غير قابل للتغيير وله نفس القيمة بغض النظر عن الوقت.

نظرًا لأنه من المفترض أن يكون الحساب البنكي كائنًا نريد تصميمه وتختلف حالته بمرور الوقت، فإننا في FP نقوم بتشفير تلك المعلومات في النوع.لذا، قد يكون "a" من النوع "IO BankAccount"، أو أي نوع أحادي آخر يتلخص بشكل أساسي في جعل "a" دالة تأخذ كمدخلات "الحالة السابقة للعالم" (أو الحالة السابقة لأسعار الفائدة المصرفية ، أو أيًا كان)، ويعيد حالة جديدة للعالم.تحديث سعر الفائدة سيكون بمثابة عملية أخرى من النوع الذي يمثل التأثير (على سبيل المثال.عملية إدخال/إخراج أخرى)، وبالتالي ستعيد "عالمًا" جديدًا، وكل ما قد يعتمد على سعر الفائدة (حالة العالم) سيكون عبارة عن بيانات من النوع الذي يعرف أنه يحتاج إلى أخذ هذا العالم كمدخل.

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

القراءة على الدولة موناد قد يكون من المفيد التعرف على كيفية تصميم "الحالة المشتركة القابلة للتغيير" بشكل بحت.

  1. هياكل البيانات غير القابلة للتغيير ليست مثل VCS.فكر في بنية البيانات غير القابلة للتغيير كملف للقراءة فقط.إذا كان للقراءة فقط، فلا يهم من يقرأ أي جزء من الملف في أي وقت، فسيقرأ الجميع المعلومات الصحيحة.

  2. هذا الجواب يتحدث عنه http://en.wikipedia.org/wiki/Monad_(functional_programming)

MVCC (التحكم في التزامن متعدد الإصدارات)

تم وصف حل المشكلة التي تشير إليها بواسطة ريتش هيكي في كتابه عروض الفيديو.

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

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

"غير قابل للتغيير" يعني بالضبط ما يلي:لا يتغير.

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

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