Monads في إرلانج


على Habré ، يمكنك العثور على العديد من المنشورات التي تكشف عن نظرية monads وممارسة تطبيقها. ومن المتوقع أن معظم هذه المقالات عن هاسكل. لن أروي النظرية للمرة التاسعة. اليوم سنتحدث عن بعض مشاكل Erlang ، طرق لحلها مع monads ، الاستخدام الجزئي للوظائف والسكر النحوي من erlando - مكتبة رائعة من فريق RabbitMQ.


مقدمة


إرلانج لديه ثبات ، ولكن لا monads * . ولكن بفضل وظيفة parse_transform وتطبيق erlando باللغة ، لا تزال هناك إمكانية لاستخدام monads في Erlang.


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


Erlando


Erlando عبارة عن مجموعة امتداد Erlang توفر لنا:


  • الاستخدام الجزئي / كاري الوظائف مع تخفيضات تشبه المخطط
  • تشبه هاسكل لا
  • استيراد - كما - السكر النحوي لاستيراد وظائف من وحدات أخرى.

ملاحظة: أخذت أمثلة التعليمات البرمجية التالية لتوضيح ميزات erlando من العرض التقديمي لـ Matthew Sackman ، مما أدى إلى تمييعها جزئيًا باستخدام الكود والتفسيرات الخاصة بي.


مجردة قص


الذهاب مباشرة إلى هذه النقطة. فكر في عدة وظائف من مشروع حقيقي:


info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties{ expiry = calculate_msg_expiry(TTL)} end. 

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


القيمة _


  • _ يمكن استخدامها في القوالب
  • يتيح لك القص استخدام _ الأنماط الخارجية
  • إذا كان خارج القالب ، يصبح معلمة للتعبير الذي يوجد به
  • الاستخدام المتعدد لـ _ ضمن نفس التعبير يؤدي إلى استبدال العديد من المعلمات في هذا التعبير
  • القص ليس بديلاً عن عمليات الإغلاق (funs)
  • يتم تقييم الحجج قبل وظيفة القطع.

يستخدم Cut _ في تعبيرات للإشارة إلى حيث يجب تطبيق التجريد. يلف القص فقط المستوى الأقرب في التعبير ، لكن القطع المتداخلة غير محظورة.
على سبيل المثال list_to_binary([1, 2, math:pow(2, _)]). list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]). إلى list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]). ولكن ليس في fun (X) -> list_to_binary([1, 2, math:pow(2, X)]) end. .


يبدو الأمر مربكًا بعض الشيء ، دعنا نعيد كتابة الأمثلة أعلاه باستخدام أداة القص:


 info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). info_all(VHostPath, Items) -> map(VHostPath, info(_, Items)). 

 backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). backing_queue_timeout(State = #q{backing_queue = BQ}) -> run_backing_queue(BQ, _:timeout(_), State). 

 reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties { expiry = calculate_msg_expiry(TTL) } end. reset_msg_expiry_fun(TTL) -> _ #message_properties { expiry = calculate_msg_expiry(TTL) }. 

ترتيب حساب الوسيطة


لتوضيح الترتيب الذي يتم به حساب الوسائط ، خذ بعين الاعتبار المثال التالي:


 f1(_, _) -> io:format("in f1~n"). test() -> F = f1(io:format("test line 1~n"), _), F(io:format("test line 2~n")). 

نظرًا لأن الوسائط يتم تقييمها قبل دالة القطع ، فسيتم عرض ما يلي:


 test line 2 test line 1 in f1 

قطع التجريد في أنواع وأنماط مختلفة من التعليمات البرمجية


  • الصفوف
     F = {_, 3}, {a, 3} = F(a). 
  • قوائم
     dbl_cons(List) -> [_, _ | List]. test() -> F = dbl_cons([33]), [7, 8, 33] = F(7, 8). 
  • تسجيل
     -record(vector, { x, y, z }). test() -> GetZ = _#vector.z, 7 = GetZ(#vector { z = 7 }), SetX = _#vector{x = _}, V = #vector{ x = 5, y = 4 } = SetX(#vector{ y = 4 }, 5). 
  • الحالات
     F = case _ of N when is_integer(N) -> N + N; N -> N end, 10 = F(5), ok = F(ok). 
  • خرائط
     test() -> GetZ = maps:get(z, _), 7 = GetZ(#{ z => 7 }), SetX = _#{x => _}, V = #{ x := 5, y := 4 } = SetX(#{ y => 4 }, 5). 
  • مطابقة القوائم وبناء البيانات الثنائية
     test_cut_comprehensions() -> F = << <<(1 + (X*2))>> || _ <- _, X <- _ >>, %% Note, this'll only be a /2 ! <<"AAA">> = F([a,b,c], [32]), F1 = [ {X, Y, Z} || X <- _, Y <- _, Z <- _, math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2) ], [{3,4,5}, {4,3,5}, {6,8,10}, {8,6,10}] = lists:usort(F1(lists:seq(1,10), lists:seq(1,10), lists:seq(1,10))). 

الايجابيات


  • أصبح الرمز أصغر ، وبالتالي فمن السهل الحفاظ عليه.
  • أصبح الرمز أكثر بساطة ومرتبة.
  • ذهب الضوضاء من funs.
  • للمبتدئين في Erlang ، من الأنسب لكتابة وظائف Get / Set.

سلبيات


  • زيادة عتبة الدخول للمطورين ذوي الخبرة Erlang ، مع تقليل عتبة الدخول للمبتدئين. الآن مطلوب من الفريق فهم القص ومعرفة بناء جملة آخر.

لا تدوين


لينة فاصلة هو بناء ملزمة حساب. لا يحتوي Erlang على نموذج حساب كسول. دعونا نتخيل ماذا سيحدث لو كان إرلانج كسولًا مثل هاسكل


 my_function() -> A = foo(), B = bar(A, dog), ok. 

لضمان أمر التنفيذ ، نحتاج إلى ربط الحسابات بشكل صريح عن طريق تحديد فاصلة.


 my_function() -> A = foo(), comma(), B = bar(A, dog), comma(), ok. 

متابعة التحويل:


 my_function() -> comma(foo(), fun (A) -> comma(bar(A, dog), fun (B) -> ok end)). 

بناءً على الاستنتاج ، فاصلة / 2 هي دالة اصطلاحية >>=/2 . يتطلب monad ثلاث وظائف فقط: >>=/2 ، return/1 و fail/1 .
كل شيء سيكون على ما يرام ، ولكن بناء الجملة هو مجرد فظيعة. نحن نطبق محولات بناء الجملة من erlando .


 do([Monad || A <- foo(), B <- bar(A, dog), ok]). 

أنواع منادس


نظرًا لأن do-block معلمة ، فيمكننا استخدام monads من أنواع مختلفة. داخل do-block ، fail/1 نشر المكالمات return/1 و fail/1 على Monad:return/1 و Monad:fail/1 على التوالي.


  • ، الكائن الدقيق الاحادي الخلية الهوية.
    monad المتطابق هو monad monost الذي لا يغير نوع القيم ولا يشارك في التحكم في عملية الحساب. يتم تطبيقه مع المحولات. يؤدي ربط التعبيرات - فاصلة البرنامج التي نوقشت أعلاه.


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


     if_safe_div_zero(X, Y, Fun) -> do([maybe_m || Result <- case Y == 0 of true -> fail("Cannot divide by zero"); false -> return(X / Y) end, return(Fun(Result))]). 

    يتوقف تقييم التعبير إذا لم يتم إرجاع أي شيء.


     {just, 6} = if_safe_div_zero(10, 5, _+4) ## 10/5 = 2 -> 2+4 -> 6 nothing = if_safe_div_zero(10, 0, _+4) 

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


     write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], case make_binary(Data) of Bin when is_binary(Bin) -> case file:open(Path, Modes1) of {ok, Hdl} -> case file:write(Hdl, Bin) of ok -> case file:sync(Hdl) of ok -> file:close(Hdl); {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> E end; {error, _} = E -> E end. 

     make_binary(Bin) when is_binary(Bin) -> Bin; make_binary(List) -> try iolist_to_binary(List) catch error:Reason -> {error, Reason} end. 


قراءة هذا غير سارة ، يشبه الشعرية رد الاتصال في JS. Error_m يأتي لانقاذ:


 write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], do([error_m || Bin <- make_binary(Data), Hdl <- file:open(Path, Modes1), Result <- return(do([error_m || file:write(Hdl, Bin), file:sync(Hdl)])), file:close(Hdl), Result]). make_binary(Bin) when is_binary(Bin) -> error_m:return(Bin); make_binary(List) -> try error_m:return(iolist_to_binary(List)) catch error:Reason -> error_m:fail(Reason) end. 

  • ، الكائن الدقيق الاحادي الخلية القائمة.
    القيم هي قوائم يمكن تفسيرها على أنها عدة نتائج محتملة لعملية حسابية واحدة. إذا كانت إحدى العمليات الحسابية تعتمد على حساب آخر ، فسيتم إجراء الحساب الثاني لكل نتيجة من النتائج الأولى ، ويتم جمع النتائج (الحساب الثاني) في قائمة.
    النظر في المثال مع ثلاثية فيثاغورس الكلاسيكية. نحسبها دون monads:
     P = [{X, Y, Z} || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)]. 

نفس الشيء مع list_m فقط:


 P = do([list_m || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), monad_plus:guard(list_m, math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)), return({X,Y,Z})]). 

  • ، الكائن الدقيق الاحادي الخلية الدولة.
    مناد الحوسبة للدولة.
    في بداية المقال ، تحدثنا عن صعوبات المبتدئين عند العمل مع حالة متغيرة. غالبًا ما يبدو الرمز كالتالي:
     State1 = init(Dimensions), State2 = plant_seeds(SeedCount, State1), {DidFlood, State3} = pour_on_water(WaterVolume, State2), State4 = apply_sunlight(Time, State3), {DidFlood2, State5} = pour_on_water(WaterVolume, State4), {Crop, State6} = harvest(State5), ... 

باستخدام محول وقص ، يمكن إعادة كتابة هذا الرمز في شكل أكثر إحكاما وقراءة:


 StateT = state_t:new(identity_m), SM = StateT:modify(_), SMR = StateT:modify_and_return(_), StateT:exec( do([StateT || StateT:put(init(Dimensions)), SM(plant_seeds(SeedCount, _)), DidFlood <- SMR(pour_on_water(WaterVolume, _)), SM(apply_sunlight(Time, _)), DidFlood2 <- SMR(pour_on_water(WaterVolume, _)), Crop <- SMR(harvest(_)), ... ]), undefined). 

  • أوميغا الكائن الدقيق الاحادي الخلية.
    على غرار list_m monad. ومع ذلك ، يتم مرور قطريا.

خطأ معالجة خفية


ربما واحدة من الميزات المفضلة لي من error_m . بغض النظر عن مكان حدوث الخطأ ، ستُرجع المنصة دائمًا إما {ok, Result} أو {error, Reason} . مثال يوضح السلوك:


 do([error_m || Hdl <- file:open(Path, Modes), Data <- file:read(Hdl, BytesToRead), file:write(Hdl, DataToWrite), file:sync(Hdl), file:close(Hdl), file:rename(Path, Path2), file:delete(Path), return(Data)]). 

Import_as


لتناول وجبة خفيفة لدينا بناء الجملة import_as السكر. يسمح لك بناء الجملة القياسي للسمة -import / 2 باستيراد وظائف من الآخرين إلى الوحدة المحلية. ومع ذلك ، لا يسمح لك بناء الجملة هذا بتعيين اسم بديل للدالة المستوردة. Import_as يحل هذه المشكلة:


 -import_as({my_mod, [{size/1, m_size}]}) -import_as({my_other_mod, [{size/1, o_size}]}) 

يتم توسيع هذه التعبيرات إلى وظائف محلية حقيقية ، على التوالي:


 m_size(A) -> my_mod:size(A). o_size(A) -> my_other_mod:size(A). 

استنتاج


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


* - في الواقع ، في Erlang monads موجودة دون erlando. تعبيرات مفصولة بفواصل هي بناء الخطية وربط الحسابات.


ملحوظة: في الآونة الأخيرة ، تم وضع علامة على مكتبة erlando من قبل المؤلفين كأرشيف لقد كتبت هذا المقال منذ أكثر من عام. ومع ذلك ، كما هو الحال الآن ، على Habré لم تكن هناك معلومات عن monads في Erlang. لتصحيح هذا الموقف ، أنا أنشر هذا المقال وإن كان متأخراً.
لاستخدام erlando في erlang> = 22 ، تحتاج إلى حل المشكلة مع erlang المهملة: get_stacktrace / 0. يمكن العثور على مثال لإصلاح في مفترقي: https://github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2


شكرا على وقتك!

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


All Articles