Olá Habr!
Às vezes, ao desenvolver serviços de rede e interfaces de usuário, é preciso lidar com cenários de interação bastante complicados, que contêm ramificações e loops. Esses cenários não se encaixam em uma máquina de estado simples - não é suficiente armazenar todos os dados no objeto de sessão, também é aconselhável rastrear a rota do sistema para entrar em um estado ou outro e, em alguns casos, ser capaz de voltar alguns passos, repetir o diálogo em um loop e assim por diante. .d. Anteriormente, para esse fim, era necessário desenvolver suas próprias estruturas de dados que imitam a máquina de empilhar ou até mesmo usar linguagens de script de terceiros. Com o advento dos recursos assíncronos em quase todas as linguagens de programação, tornou-se possível escrever scripts na mesma linguagem em que o serviço foi gravado. O script, com sua pilha e variáveis locais, é na verdade uma sessão do usuário, ou seja, armazena os dados e a rota. Por exemplo, goroutine com bloqueio de leitura do canal resolve facilmente esse problema, mas primeiro o fio verde
não está
livre e, em segundo lugar, escrevemos em Rust, onde não há fios verdes, mas existem
geradores e
async / wait .
Por exemplo, escreveremos um http-bot simples que exibe um formulário html no navegador, fazendo perguntas ao usuário até que ele responda que se sente bem. O programa é um servidor http simples e de thread único; escrevemos o script bot na forma de um gerador Rust. Deixe-me lembrá-lo de que os
geradores JavaScript permitem a troca bidirecional de dados, ou seja, dentro do gerador, você pode passar a pergunta:
my_generator.next (my_question);e retorne a resposta a partir dele:
yield my_response;No Rust, a transferência de valores dentro do gerador ainda não foi implementada (a função
resume () não possui parâmetros, embora exista
uma discussão para corrigi-lo), por isso organizamos a troca de dados por meio de uma célula compartilhada, na qual se encontra a estrutura com os dados recebidos e enviados. O script do nosso bot é criado pela função
create_scenario () , que retorna uma instância do gerador, essencialmente o fechamento no qual o parâmetro é movido - um ponteiro para a
célula de dados
udata . Para cada sessão de usuário, armazenamos nossa própria célula com dados e nossa própria instância do gerador, com seu próprio estado da pilha e os valores das variáveis locais.
#[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 (); } }
Cada etapa do script consiste em ações simples - obtenha um link para o conteúdo da célula, salve a entrada do usuário em variáveis locais, defina o texto da resposta e dê controle para o exterior, através do
rendimento . Como pode ser visto no código, nosso gerador retorna uma tupla vazia () e todos os dados são transmitidos através de uma célula comum com um contador de referência
Ref <Cell <... >> . Dentro do gerador, você precisa garantir que o empréstimo do conteúdo da célula
borrow () não ultrapasse o ponto de
retorno , caso contrário, será impossível atualizar os dados do lado de fora do gerador - portanto, infelizmente, você não pode escrever uma vez no início do algoritmo,
udata_mut = udata.borrow_mut () e você precisa pedir emprestado um valor após cada rendimento.
Implementamos nosso próprio loop de eventos (leitura do soquete) e, para cada solicitação recebida, criamos uma nova sessão de usuário ou localizamos a existente por sid, atualizando os dados:
let mut udata: UserData = read_udata(&mut stream); let mut sid = udata.sid.clone(); let session; if sid == "" {
Em seguida, transferimos o controle dentro do gerador correspondente e atualizamos os dados atualizados de volta para o soquete. Na última etapa, quando o script inteiro é concluído, excluímos a sessão do hashmap e ocultamos o campo de entrada da página html usando um script 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);
Código de trabalho completo aqui:
github.com/epishman/habr_samples/blob/master/chatbot/main.rsPeço desculpas pela análise de http do "farm coletivo", que nem suporta entrada em cirílico, mas tudo é feito usando ferramentas de linguagem padrão, sem estruturas, bibliotecas e sms. Eu realmente não gosto de clonar strings, e o próprio script não parece muito compacto devido ao uso intenso de
borrow_mut () e
clone () . Provavelmente, rastamans experientes serão capazes de simplificar isso (por exemplo, usando macros). O principal é que o problema seja resolvido por meios mínimos, e espero que em breve recebamos um conjunto completo de ferramentas assíncronas em uma versão estável.
PS
Para compilar, você precisa de uma compilação noturna:
rustup default nightly rustup update
Camaradas do inglês Stack Overflow me ajudaram a lidar com os geradores:
stackoverflow.com/questions/56460206/how-can-i-transfer-some-values-into-a-rust-generator-at-each-step