مرحبا يا هبر!
في بعض الأحيان ، عند تطوير خدمات الشبكة وواجهات المستخدم ، يتعين على المرء التعامل مع سيناريوهات التفاعل المعقدة إلى حد ما التي تحتوي على فروع وحلقات. لا تتوافق مثل هذه السيناريوهات مع جهاز حالة بسيط - لا يكفي تخزين جميع البيانات في كائن الجلسة ، وينصح أيضًا بتتبع مسار النظام للوصول إلى حالة أو أخرى ، وفي بعض الحالات تكون قادرًا على الرجوع إلى بضع خطوات ، وتكرار الحوار في حلقة ، وما إلى ذلك. .D. في السابق ، لهذا الغرض ، كان يتعين عليك تطوير هياكل البيانات الخاصة بك التي تقلد الجهاز المكدس ، أو حتى استخدام لغات البرمجة النصية لجهة خارجية. مع ظهور قدرات غير متزامنة في جميع لغات البرمجة تقريبًا ، أصبح من الممكن كتابة البرامج النصية بنفس اللغة التي كتبت بها الخدمة. البرنامج النصي ، مع مكدسه والمتغيرات المحلية ، هو في الواقع جلسة مستخدم ، أي أنه يخزن كل من البيانات والطريق. على سبيل المثال ، goroutine مع حظر القراءة من القناة يحل هذه المشكلة بسهولة ، ولكن أولاً ، الخيط الأخضر
ليس مجانيًا ، وثانياً ، نكتب في Rust ، حيث لا توجد خيوط خضراء ، ولكن هناك
مولدات ومتزامنة / تنتظر .
على سبيل المثال ، سنكتب http-bot بسيط يعرض نموذج html في المستعرض ، ونطرح أسئلة على المستخدم حتى يجيب أنه يشعر بحالة جيدة. البرنامج هو أبسط خادم بروتوكول نقل الملفات أحادي الخيوط ؛ نكتب البرنامج النصي بوت في شكل مولد الصدأ. اسمحوا لي أن أذكرك بأن
مولدات جافا سكريبت تسمح بتبادل البيانات في اتجاهين ، أي أنه داخل المولد يمكنك تمرير السؤال:
my_generator.next (my_question)؛وإرجاع الاستجابة منه:
yield my_response؛في Rust ، لم يتم تنفيذ نقل القيم داخل المولد
( لا تحتوي الدالة
استئناف () على أية معلمات ، على الرغم من وجود
مناقشة لإصلاح هذا) ، لذلك ننظم تبادل البيانات من خلال خلية مشتركة ، حيث توجد البنية مع البيانات المستلمة والمرسلة. يتم إنشاء النص
البرمجي لبوتنا بواسطة الدالة
create_scenario () ، والتي تُرجع مثيل المولد ، وهي أساسًا الإغلاق الذي يتم فيه نقل المعلمة - مؤشر إلى
خلية بيانات
udata . لكل جلسة مستخدم ، نقوم بتخزين الخلية الخاصة بنا مع البيانات ومثيلنا الخاص بالمولد ، مع حالة المكدس الخاصة به وقيم المتغيرات المحلية.
#[derive(Default, Clone)] struct UserData { sid: String, msg_in: String, msg_out: String, script: String, } type UserDataCell = Rc<RefCell<UserData>>; struct UserSession { udata: UserDataCell, scenario: Pin<Box<dyn Generator<Yield = (), Return = ()>>>, } type UserSessions = HashMap<String, UserSession>; fn create_scenario(udata: UserDataCell) -> impl Generator<Yield = (), Return = ()> { move || { let uname; let mut umood; udata.borrow_mut().msg_out = format!("Hi, what is you name ?"); yield (); uname = udata.borrow().msg_in.clone(); udata.borrow_mut().msg_out = format!("{}, how are you feeling ?", uname); yield (); 'not_ok: loop { umood = udata.borrow().msg_in.clone(); if umood.to_lowercase() == "ok" { break 'not_ok; } udata.borrow_mut().msg_out = format!("{}, think carefully, maybe you're ok ?", uname); yield (); umood = udata.borrow().msg_in.clone(); if umood.to_lowercase() == "ok" { break 'not_ok; } udata.borrow_mut().msg_out = format!("{}, millions of people are starving, maybe you're ok ?", uname); yield (); } udata.borrow_mut().msg_out = format!("{}, good bye !", uname); return (); } }
تتكون كل خطوة من خطوات البرنامج النصي من إجراءات بسيطة - احصل على رابط لمحتويات الخلية ، واحفظ مدخلات المستخدم في المتغيرات المحلية ، واضبط نص الاستجابة واتحكم في الخارج ، من خلال
العائد . كما يتبين من الكود ، فإن مولدنا يعرض tuple () فارغًا ، ويتم إرسال جميع البيانات عبر خلية شائعة باستخدام عداد مرجعي
Ref <Cell <... >> . داخل المولد ، تحتاج إلى التأكد من أن اقتراض محتويات خلية
الاقتراض () لا يعبر
عن نقطة
العائد ، وإلا فلن يكون من المستحيل تحديث البيانات من خارج المولد - لذلك ، للأسف ، لا يمكنك الكتابة مرة واحدة في بداية الخوارزمية ،
دع udata_mut = udata.borrow_mut () عليك أن تقترض قيمة بعد كل عائد.
ننفذ حلقة الأحداث الخاصة بنا (القراءة من المقبس) ، ولكل طلب وارد ، إما أن ننشئ جلسة مستخدم جديدة أو نعثر على جلسة موجودة عن طريق وضع التحيز الجانبي ، ونقوم بتحديث البيانات الموجودة فيها:
let mut udata: UserData = read_udata(&mut stream); let mut sid = udata.sid.clone(); let session; if sid == "" {
بعد ذلك ، ننقل التحكم داخل المولد المقابل ، ونحدّث البيانات المحدّثة مرة أخرى إلى المقبس. في الخطوة الأخيرة ، عند اكتمال البرنامج النصي بأكمله ، نقوم بحذف الجلسة من التجزئة وإخفاء حقل الإدخال من صفحة html باستخدام برنامج نصي js.
udata = match session.scenario.as_mut().resume() { GeneratorState::Yielded(_) => session.udata.borrow().clone(), GeneratorState::Complete(_) => { let mut ud = sessions.remove(&sid).unwrap().udata.borrow().clone(); ud.script = format!("document.getElementById('form').style.display = 'none'"); ud } }; write_udata(&udata, &mut stream);
رمز العمل الكامل هنا:
github.com/epishman/habr_samples/blob/master/chatbot/main.rsأعتذر عن تحليل http "مزرعة جماعية" ، والذي لا يدعم حتى مدخلات السيريلية ، لكن كل شيء يتم باستخدام أدوات اللغة القياسية ، بدون أطر عمل ، ومكتبات ، ورسائل نصية قصيرة. لا أحب حقًا استنساخ السلاسل ، ولا يبدو النص نفسه مضغوطًا جدًا بسبب الاستخدام المكثف
للاقتراض () والنسخ
() . من المحتمل أن يكون بإمكان الرستمان ذوي الخبرة تبسيط ذلك (على سبيل المثال ، استخدام وحدات الماكرو). الشيء الرئيسي هو أن المشكلة قد تم حلها بأدنى الطرق ، وآمل أن نتلقى قريبًا مجموعة كاملة من الأدوات غير المتزامنة في إصدار ثابت.
PS
لتجميع ، تحتاج إلى بناء ليلا:
rustup default nightly rustup update
ساعدني الرفاق من English Stack Overflow في التعامل مع المولدات:
stackoverflow.com/questions/56460206/how-can-i-transfer-some-values-into-a-rust-generator-at-each-step