الأداء غير المفهوم لجدولة متعددة

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



من أحد المترجمين : يشير عنوان التقرير إلى مقالة كتبها يوجين فينر ، "الفعالية غير المفهومة للرياضيات في العلوم الطبيعية" .


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


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


في بعض الأحيان ، عندما أقول هذا ، فإن الجمهور لا يصدقني ، لكنك بالفعل في JuliaCon ، لذلك أنت على دراية بما يحدث ، لذلك سأركز على سبب حدوث ذلك في رأيي.


لكن بالنسبة للمبتدئين - أحد الأمثلة المفضلة لدي.



على الشريحة هو نتيجة عمل كريس Rakaukas. يكتب جميع أنواع الحزم المعممة للغاية لحل المعادلات التفاضلية. يمكنك تغذية الأرقام المزدوجة ، أو BigFloat ، كل ما تريد. وبطريقة ما قرر أنه يريد أن يرى خطأ نتيجة التكامل. وكان هناك حزمة القياسات التي يمكن أن تتبع كل من قيمة الكمية المادية وانتشار الخطأ من خلال سلسلة من الصيغ. تدعم هذه الحزمة أيضًا بناء الجملة الأنيق لقيم عدم اليقين باستخدام حرف Unicode ± . يظهر هنا على الشريحة أن تسارع الجاذبية وطول البندول والسرعة الأولية وزاوية الانحراف كلها معروفة بنوع من الأخطاء. لذلك ، يمكنك تحديد بندول بسيط ، وتمرير معادلاته للحركة من خلال ODE حلالا و - بام! - كل شيء يعمل . وترى رسم بياني مع عدم دقة الشارب. وما زلت لا أظهر أن رمز رسم الرسم البياني معمم أيضًا ، وقد وضعت القيمة مع وجود خطأ في Measurement.jl واحصل على رسم بياني به أخطاء.


مستوى التوافق بين الحزم المختلفة وتعميم الكود هو ببساطة حامل للعقل. كيف يعمل فقط ؟ اتضح نعم.


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


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


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


مقدمة. جدولة متعددة مقابل وظيفة التحميل الزائد


الآن يجب أن أشير إلى الحمولة الزائدة للوظائف في C ++ أو Java ، حيث إنني دائمًا ما أطرح الأسئلة عليها. للوهلة الأولى ، لا يختلف عن الجدولة المتعددة. ما هو الفرق ولماذا وظيفة الزائد أسوأ؟


سأبدأ بمثال على جوليا:


 abstract type Pet end struct Dog <: Pet; name::String end struct Cat <: Pet; name::String end function encounter(a::Pet, b::Pet) verb = meets(a, b) println("$(a.name) meets $(b.name) and $verb") end meets(a::Dog, b::Dog) = "sniffs" meets(a::Dog, b::Cat) = "chases" meets(a::Cat, b::Dog) = "hisses" meets(a::Cat, b::Cat) = "slinks" 

نقوم بتعريف النوع التجريدي Pet ، ونقدم الأنواع الفرعية من Dog and Cat لذلك ، لديهم حقل اسم (الكود يتكرر قليلاً ، لكنه مقبول) ويحدد الوظيفة العامة لـ "الاجتماع" التي تأخذ كائنين من النوع Pet الوسائط. فيه ، نحسب أولاً "الإجراء" الذي تحدده نتيجة استدعاء وظيفة meet() المعممة ، ثم نطبع الجملة التي تصف الاجتماع. في دالة meets() ، نستخدم الإرسال المتعدد لتحديد الإجراء الذي يقوم به حيوان عندما يواجه الآخر.


أضف اثنين من الكلاب واثنين من القطط وشاهد نتائج الاجتماع:


 fido = Dog("Fido") rex = Dog("Rex") whiskers = Cat("Whiskers") spots = Cat("Spots") encounter(fido, rex) encounter(rex, whiskers) encounter(spots, fido) encounter(whiskers, spots) 

الآن سنقوم "بترجمة" نفسه إلى C ++ حرفيًا قدر الإمكان. حدد فئة Pet مع حقل name - في C ++ يمكننا القيام بذلك (بالمناسبة ، تتمثل إحدى ميزات C ++ في أنه يمكن إضافة حقول البيانات إلى أنواع الملخصات ، ثم نحدد الدالة meets() الأساسية ، ونعرّف وظيفة encounter() لكائنين من النوع Pet و ، أخيرًا ، حدد الفئات المشتقة من Dog and Cat وقم بإجراء التحميل الزائد meets() لهم:


 class Pet { public: string name; }; string meets(Pet a, Pet b) { return "FALLBACK"; } void encounter(Pet a, Pet b) { string verb = meets(a, b); cout << a.name << " meets " << b. name << " and " << verb << endl; } class Cat : public Pet {}; class Dog : public Pet {}; string meets(Dog a, Dog b) { return "sniffs"; } string meets(Dog a, Cat b) { return "chases"; } string meets(Cat a, Dog b) { return "hisses"; } string meets(Cat a, Cat b) { return "slinks"; } 

إن الوظيفة main() ، كما في كود جوليا ، تنشئ الكلاب والقطط وتجعلهم يجتمعون:


 int main() { Dog fido; fido.name = "Fido"; Dog rex; rex.name = "Rex"; Cat whiskers; whiskers.name = "Whiskers"; Cat spots; spots.name = "Spots"; encounter(fido, rex); encounter(rex, whiskers); encounter(spots, fido); encounter(whiskers, spots); return 0; } 

لذلك ، إيفاد متعددة ضد الحمولة الزائدة وظيفة. غونغ!



ما رأيك سيعود رمز مع إيفاد متعددة؟


جوليا pets.jl
 Fido meets Rex and sniffs Rex meets Whiskers and chases Spots meets Fido and hisses Whiskers meets Spots and slinks 

تلتقي الحيوانات وتشممها وتسممها وتلعب اللحاق بالركب - كما كان مقصودًا.


$ g ++ -o pets pets.cpp && ./pets
 Fido meets Rex and FALLBACK Rex meets Whiskers and FALLBACK Spots meets Fido and FALLBACK Whiskers meets Spots and FALLBACK 

في جميع الحالات ، يتم إرجاع الخيار "احتياطي".


لماذا؟ لأن هذه هي الطريقة التي تعمل الحمولة الزائدة. إذا نجح الإرسال المتعدد ، فسيتم meets(a, b) encounter() داخلي" encounter() بالأنواع المحددة التي كان لدى a و b في وقت المكالمة. ولكن يتم تطبيق التحميل الزائد ، لذلك meets() يسمى للأنواع الثابتة a و b ، وكلاهما في هذه الحالة هما Pet .


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


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


الآن دعونا نرى مثل هذا الجدول. أتمنى أن تجدها ذات مغزى:


نوع الجدولةبناء الجملةإرسال الحججدرجة من التعبيرفرصة معبرة
لاf (x 1 ، x 2 ، ...){}يا (1)ثابت
وحيد× 1. و2 ، ...)1 }O (| X 1 |)خطي
مضاعفf (x 1 ، x 2 ، ...)1 ، × 2 ، ...}O (| X 1 | | X 2 | ...)الأسي

في اللغات دون إرسال ، يمكنك ببساطة كتابة f(x, y, ...) ، يتم إصلاح أنواع جميع الوسائط ، أي استدعاء f() هي دعوة لوظيفة واحدة f() ، والتي قد تكون في البرنامج. درجة التعبيرية ثابتة: استدعاء f() دائمًا يفعل شيئًا واحدًا فقط. كانت عملية الإرسال الأحادي انفراجة كبيرة في الانتقال إلى OOP في التسعينيات والألفينيات. عادةً ما يتم استخدام بناء جملة نقطة ، والذي يحبه الناس حقًا. وتظهر فرصة تعبيرية إضافية: يتم إرسال المكالمة وفقًا لنوع الكائن × 1 . تتميز الفرصة المعبرة بقوة المجموعة | X 1 | أنواع لها طريقة f() . في الإرسال المتعدد ، ومع ذلك ، فإن عدد الخيارات المحتملة للدالة f() يساوي قوة المنتج الديكارتي لمجموعات الأنواع التي يمكن أن تنتمي إليها الوسيطات. في الواقع ، بالطبع ، بالكاد يحتاج أي شخص إلى العديد من الوظائف المختلفة في برنامج واحد. لكن النقطة الأساسية هنا هي أن المبرمج يتم إعطاء طريقة بسيطة وطبيعية لاستخدام أي عنصر من هذا التنوع ، وهذا يؤدي إلى نمو هائل للفرص.


الجزء 1. البرمجة العامة


دعونا نتحدث عن الشفرة المعممة - السمة الرئيسية للإرسال المتعدد.


فيما يلي مثال (مصطنع تمامًا) للرمز العام:


 using LinearAlgebra function inner_sum(A, vs) t = zero(eltype(A)) for v in vs t += inner(v, A, v) #  ! end return t end inner(v, A, w) = dot(v, A * w) #    

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


لذلك ، "انظر يا أمي ، إنها تعمل":


 julia> A = rand(3, 3) 3×3 Array{Float64,2}: 0.934255 0.712883 0.734033 0.145575 0.148775 0.131786 0.631839 0.688701 0.632088 julia> vs = [rand(3) for _ in 1:4] 4-element Array{Array{Float64,1},1}: [0.424535, 0.536761, 0.854301] [0.715483, 0.986452, 0.82681] [0.487955, 0.43354, 0.634452] [0.100029, 0.448316, 0.603441] julia> inner_sum(A, vs) 6.825340887556694 

لا شيء خاص ، فإنه يحسب بعض القيمة. لكن - يتم كتابة الكود بأسلوب معمم وسيعمل مع أي A و vs ، إذا كان من الممكن إجراء العمليات المقابلة عليها فقط.


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


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


 import Base: size, getindex, * struct OneHotVector <: AbstractVector{Int} len :: Int ind :: Int end size(v::OneHotVector) = (v.len,) getindex(v::OneHotVector, i::Integer) = Int(i == v.ind) 

في الواقع ، هذا هو حقا تعريف النوع الكامل من الحزمة التي تضيفه. ومع هذا التعريف ، تعمل inner_sum() أيضًا:


 julia> vs = [OneHotVector(3, rand(1:3)) for _ in 1:4] 4-element Array{OneHotVector,1}: [0, 1, 0] [0, 0, 1] [1, 0, 0] [1, 0, 0] julia> inner_sum(A, vs) 2.6493739294755123 

لكن بالنسبة للمنتج القياسي ، يتم استخدام تعريف عام هنا - بالنسبة لهذا النوع من البيانات فهو بطيء ، وليس رائعًا!


لذلك ، تعمل التعريفات العامة ، ولكن ليس دائمًا بالطريقة المثلى ، وقد تواجه ذلك من حين لآخر عند استخدام Julia: "حسنًا ، يتم استدعاء تعريف عام ، ولهذا السبب يعمل رمز GPU هذا للساعة الخامسة ..."


في inner() بشكل افتراضي ، يُطلق على التعريف العام لمنتج المصفوفة بواسطة متجه ، والذي عند ضربه بضرب متجه وحدوي ، يُرجع نسخة من أحد أعمدة النوع Vector{Float64} . ثم يسمى التعريف العام لنقطة المنتج العددية بالنقطة الأحادية وهذا العمود الذي يقوم بالكثير من الأعمال غير الضرورية. في الواقع ، لكل عنصر يتم التحقق منه "هل أنت مساوٍ لمكون واحد؟ وأنت؟" إلخ


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


 *(A::AbstractMatrix, v::OneHotVector) = A[:, v.ind] 

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


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


 inner(v::OneHotVector, A, w::OneHotVector) = A[v.ind, w.ind] 

هذا هو الكفاءة الفائقة المخادع الموعودة. وكل ما هو مطلوب هو تحديد هذه الطريقة inner() .


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


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


والخيار الثالث - تريد فقط أن يكون لها نفس اسم الوظيفة ، ولكن مع سلوك مختلف لأنواع مختلفة من البيانات - على سبيل المثال ، أن تتصرف الوظيفة بشكل مختلف عند العمل مع القواميس والمصفوفات.


كيفية الحصول على سلوك مماثل في لغات الإرسال واحدة؟ إنه ممكن ، لكنه صعب. المشكلة: عند التحميل الزائد على الوظيفة * كان من الضروري إرسالها في الوسيطة الثانية ، وليس في الأولى. يمكنك القيام بالإرسال المزدوج: أولاً ، الإرسال بواسطة الوسيطة الأولى واستدعاء طريقة AbstractMatrix.*(v) . وهذه الطريقة ، بدورها ، تستدعي شيئًا مثل v.__rmul__(A) ، أي أصبحت الوسيطة الثانية في المكالمة الأصلية هي الكائن الذي يتم استدعاء أسلوبه بالفعل. __rmul__ مأخوذ من Python ، حيث يكون هذا السلوك نمطًا قياسيًا ، لكن يبدو أنه يعمل فقط من أجل الجمع والضرب. أي يتم حل مشكلة الإرسال المزدوج إذا أردنا استدعاء دالة تسمى + أو * ، وإلا - للأسف ، لا في أيامنا هذه. في C ++ ولغات أخرى - تحتاج إلى بناء الدراجة الخاصة بك.


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


الجزء 2. أنواع عامة


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


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


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


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


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


لذلك ، كلا الخيارين ما إلى ذلك. هناك ، مع ذلك ، حلول أخرى:


  • ضع الوظيفة في وظيفة خارجية بدلاً من طريقة الفصل - انتقل إلى f(x, y) بدلاً من xf(y) . ولكن بعد ذلك يضيع السلوك المعمم.
  • بصق على إعادة استخدام رمز (ويبدو لي ، وهذا يحدث في كثير من الحالات). ما عليك سوى نسخ فئة RGB غريبة وإضافة ما هو مفقود.

الميزة الرئيسية لجوليا من حيث إعادة استخدام الكود يتم تقريبًا تقريبًا إلى حقيقة أن الطريقة محددة خارج النوع . هذا كل شيء. قم بنفس الشيء بلغات الإرسال المفرد - ويمكن إعادة استخدام الأنواع بنفس السهولة. القصة الكاملة مع "دعونا نجعل الأساليب جزءًا من الفصل" هي فكرة في الواقع. صحيح ، هناك نقطة جيدة - استخدام الطبقات ومساحات الأسماء. إذا كتبت xf(y) - f() ليس مطلوبًا أن يكون في مساحة الاسم الحالية ، فيجب البحث عنه في مساحة الاسم x . نعم ، إنه أمر جيد - لكن هل يستحق كل المشاكل الأخرى؟ لا اعرف في رأيي ، لا (على الرغم من أن رأيي ، كما تعتقد ، منحازة قليلاً).


خاتمة. مشكلة التعبير


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


يمكن تقليل المشكلة إلى حد ما إلى ما يلي:


  1. هل من الممكن بسهولة وخالية من الأخطاء إضافة أنواع البيانات الجديدة التي الأساليب الموجودة قابلة للتطبيق و
  2. هل من الممكن إضافة عمليات جديدة على الأنواع الموجودة .

(1) يتم بسهولة في لغات وجوه المنحى وصعبة في وظيفية ، (2) - بالعكس. في هذا المعنى ، يمكننا فقط التحدث عن ازدواجية نهج OOP و FP.


في لغات الإرسال المتعددة ، كلتا العمليتين سهلة. (1) , (2) — . , . ( https://en.wikipedia.org/wiki/Expression_problem ), . ? , , . , " , " — " " . " , " , , .


, . , , — .


, Julia ( ), . .

Source: https://habr.com/ru/post/ar468609/


All Articles