Hallo Habr!
Bei der Entwicklung von Netzwerkdiensten und Benutzeroberflächen müssen manchmal komplizierte Interaktionsszenarien mit Verzweigungen und Schleifen behandelt werden. Solche Szenarien passen nicht in eine einfache Zustandsmaschine - es reicht nicht aus, alle Daten im Sitzungsobjekt zu speichern. Es ist auch ratsam, die Route des Systems zu verfolgen, um in den einen oder anderen Zustand zu gelangen. In einigen Fällen können Sie einige Schritte zurückgehen, den Dialog in einer Schleife wiederholen und so weiter. .d. Zuvor mussten Sie zu diesem Zweck eigene Datenstrukturen entwickeln, die den Stack-Computer imitieren, oder sogar Skriptsprachen von Drittanbietern verwenden. Mit dem Aufkommen asynchroner Funktionen in fast allen Programmiersprachen ist es möglich geworden, Skripte in derselben Sprache zu schreiben, in der der Dienst geschrieben ist. Das Skript mit seinem Stapel und seinen lokalen Variablen ist eigentlich eine Benutzersitzung, dh es speichert sowohl Daten als auch die Route. Zum Beispiel löst Goroutine mit blockierendem Lesen aus dem Kanal dieses Problem leicht, aber erstens ist der grüne Thread
nicht frei , und zweitens schreiben wir in Rust, wo es keine grünen Threads gibt, sondern
Generatoren und
Async / Warten .
Zum Beispiel schreiben wir einen einfachen http-Bot, der ein HTML-Formular im Browser anzeigt und dem Benutzer Fragen stellt, bis er antwortet, dass er sich gut fühlt. Das Programm ist der einfachste Single-Threaded-http-Server, wir schreiben das Bot-Skript in Form eines Rust-Generators. Ich möchte Sie daran erinnern, dass
JavaScript-Generatoren den
bidirektionalen Datenaustausch ermöglichen, dh , innerhalb des Generators können Sie die Frage übergeben:
my_generator.next (my_question);und
gib die Antwort zurück:
return my_response;In Rust wurde die Übertragung von Werten innerhalb des Generators noch nicht implementiert (die Funktion
resume () hat keine Parameter, obwohl es
eine Diskussion gibt , um dies zu beheben), daher organisieren wir den Datenaustausch über eine gemeinsam genutzte Zelle, in der die Struktur mit den empfangenen und gesendeten Daten liegt. Das Skript unseres Bots wird von der Funktion
create_scenario () erstellt , die eine Instanz des Generators zurückgibt, im Wesentlichen den Abschluss, in den der Parameter verschoben wird - einen Zeiger auf die
udata-Datenzelle . Für jede Benutzersitzung speichern wir unsere eigene Zelle mit Daten und unserer eigenen Instanz des Generators, mit dem eigenen Status des Stapels und den Werten lokaler Variablen.
#[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 (); } }
Jeder Schritt des Skripts besteht aus einfachen Aktionen - einen Link zum Inhalt der Zelle abrufen, Benutzereingaben in lokalen Variablen speichern, den Antworttext festlegen und durch
Ertrag die Kontrolle nach außen
geben . Wie aus dem Code ersichtlich ist, gibt unser Generator ein leeres Tupel () zurück, und alle Daten werden durch eine gemeinsame Zelle mit einem Referenzzähler
Ref <Cell <... >> übertragen . Innerhalb des Generators müssen Sie sicherstellen, dass das Ausleihen des Inhalts der
bor () -Zelle die Fließgrenze nicht überschreitet. Andernfalls können die Daten nicht von außerhalb des Generators aktualisiert werden. Daher können Sie zu Beginn des Algorithmus leider nicht einmal schreiben.
Lassen Sie udata_mut = udata.borrow_mut () und Sie müssen nach jeder Rendite einen Wert ausleihen.
Wir implementieren unsere eigene Ereignisschleife (Lesen aus dem Socket) und erstellen für jede eingehende Anforderung entweder eine neue Benutzersitzung oder suchen die vorhandene per Sid und aktualisieren die darin enthaltenen Daten:
let mut udata: UserData = read_udata(&mut stream); let mut sid = udata.sid.clone(); let session; if sid == "" {
Als nächstes übertragen wir die Steuerung in den entsprechenden Generator und aktualisieren die aktualisierten Daten zurück in den Socket. Im letzten Schritt, wenn das gesamte Skript abgeschlossen ist, löschen wir die Sitzung aus der Hashmap und verbergen das Eingabefeld mithilfe eines js-Skripts auf der HTML-Seite.
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);
Vollständiger Arbeitscode hier:
github.com/epishman/habr_samples/blob/master/chatbot/main.rsIch entschuldige mich für die http-Analyse der „Kollektivfarm“, die nicht einmal kyrillische Eingaben unterstützt, aber alles wird mit Standard-Sprachwerkzeugen ohne Frameworks, Bibliotheken und SMS durchgeführt. Ich mag es nicht wirklich, Strings zu klonen, und das Skript selbst sieht aufgrund der
häufigen Verwendung von
bor_mut () und
clone () nicht sehr kompakt aus. Wahrscheinlich erfahrene Rastamane können dies vereinfachen (z. B. mithilfe von Makros). Die Hauptsache ist, dass das Problem mit minimalen Mitteln gelöst wird, und ich hoffe, dass wir bald einen vollständigen Satz asynchroner Tools in einer stabilen Version erhalten.
PS
Zum Kompilieren benötigen Sie einen nächtlichen Build:
rustup default nightly rustup update
Genossen vom englischen Stapelüberlauf halfen mir, mit den Generatoren umzugehen:
stackoverflow.com/questions/56460206/how-can-i-transfer-some-values-into-a-rust-generator-at-each-step