Primeiros passos com Rust

Introdução

A linguagem Rust (https://www.rust-lang.org) é uma linguagem de programação que traz umas ideias novas bem interessantes para tapar o abismo que existe entre o C e C++ e (quase) todas as outras linguagens populares, combinando alto desempenho e abstrações leves com segurança para que uma operação errada não corrompa a memória.

Só isso já seria interessante depois de mais de uma década de interpretadores, runtimesvirtual machines, JITs e tal. Mas o mais interessante é a seu conceito de empréstimo de ponteiros (os tais de borrows).

Depois que declararam a linguagem estável na versão 1.0 em 2015, achei que valeria a pena experimentar um pouco com ela. Resolvi fazer uma tradução do tutorial de Scheme daqui http://www.inf.ufrgs.br/~vbuaraujo/blog/?entry=20160414-tour-de-scheme-2 para Rust. Um código simples é melhor para começar, porque o PdfPageCount que eu usava para comparar linguagens é meio assustador para iniciar numa linguagem totalmente nova.

O código está disponível em https://bitbucket.org/marcuscf/playground na pasta rust/agenda. Ele não é de forma alguma uma transcrição exata da versão Scheme: mudei o formato de arquivo para algo que eu pudesse manipular em poucas linhas de código, fundi funções que eu achei muito curtas (parece ser tradição em Scheme) e naturalmente a estrutura de dados usada não foi uma lista encadeada (outra tradição de Lisp, Scheme e demais linguagens funcionais, mas não é bem a coisa mais eficiente do mundo…). Aviso que não pensei muito na segurança na leitura de dados mal formatados. É só um teste da linguagem, não um programa super-robusto para controlar aviões!

Análise

Comecei da mesma forma que na versão Scheme, com funções de leitura do teclado e o menu principal. Até aí sem muitos problemas.

A primeira supresa veio na declaração da variável global “contatos”. Com let, erro. Vamos olhar o manual: para globais temos staticconst. Vai ser static então. Erro. Não pode ter static com destrutores (trait chamada Drop), não pode chamar funções complicadas na inicialização (pelo menos não na versão atual, segundo a mensagem de erro) e se declarar como mutável (mut), caímos na parte unsafe da linguagem. Até que está certo, uma variável global sendo alterada por múltiplas funções e múltiplas threads não é muito seguro. E Rust se esforça para garantir várias seguranças em relação a acesso concorrente em tempo de compilação!

Como variável global não é boa prática há umas boas décadas em qualquer linguagem de programação, não me importei em criar todas as funções recebendo um parâmetro do tipo &mut Vec e deixando a variável dentro da função main mesmo. Mas me importei, sim, em não saber como declarar algo que seja visível em todas as funções.

Por isso explorei a possibilidade de usar uma variável local da thread. Como o programa não seria multithreaded, seria o mesmo efeito de uma global. O modo como a variável deve ser obtida é interessante: com o método .with(), você passa uma função e somente dentro dessa função você poderá usar a variável. No fim, fiz isso num só local para testar como funcionava e no resto do programa continuei com a passagem de parâmetros como já estava fazendo antes.

A mutabilidade do Vec global eu consegui com a struct RefCell, porque a macro thread_local! não permite adicionar “mut” depois do static (e mesmo se permitisse, poderia haver outros problemas, como a exigência de blocos unsafe{}). No fim, a declaração ficou assim:

thread_local!(static CONTATOS: RefCell<Vec> = RefCell::new(vec![]));

(Caramba!)

e o uso (dentro de uma função) ficou assim:

CONTATOS.with(|contatos| { // ou |contatos: &RefCell|
    let contatos_mut = &mut *contatos.borrow_mut(); // o asterisco é opcional
    // (...)
});

(Caramba!)

Entendeu agora por que eu preferi continuar passando parâmetros em vez de fazer cada função obter a variável thread_local? Mas não deixei de usar a thread_local porque queria guardar um exemplo de forma de uso.

É, digamos que cada coisinha é bem explícita: a mutabilidade, os ponteiros, e cada vez que pegamos ponteiros emprestados (borrow). O asterisco ao fazer o borrow_mut() é opcional porque o resultado dessa função tem Deref automático. Uma das poucas coisas automáticas com ponteiros são a trait Deref e a chamada de método com o ponto. O resto todo é feito manualmente. À primeira vista me pareceu um design adequado: poucas coisas implícitas mas com alguns atalhos para os casos mais comuns.

As mensagens de erro do compilador são bem informativas. Apesar de não poderem exatamente ensinar a linguagem, elas informam muito bem o que o compilador estava esperando (um tipo, por exemplo) e onde o seu programa começou a diferir do esperado. Muitas vezes as mensagens são acompanhadas de dicas como “Quem sabe faltou importar o nome da trait” ou “Use tal sintaxe para solucionar esse tipo de situação”. Bem melhor que outros compiladores que só dizem algo como “Identificador desconhecido” ou “Tem algo errado mais ou menos aqui”.

A segunda surpresa foi o borrow checker, que é menos refinado do que eu pensava. Se você escreve uma chamada simples como

umaString.truncate(umaString.len() - 1)

ele já dá erro dizendo que o borrow mutável do truncate() não pode ser feito ao mesmo tempo que o borrow imutável do .len(). O mais lógico é que o len() fosse chamado e devolvesse o ponteiro para poder ser usado no truncate(), mas a implementação atual não leva isso em consideração. Para resolver, tem que chamar o len() antes, salvar o resultado numa variável e na instrução seguinte chamar o truncate().

No fim descobri que eu queria mesmo era chamar trim(), só não tinha encontrado o método na documentação até então. Até porque as linhas lidas pelo console no Windows estavam vindo com “\r\n” (e não só “\n”), então tirar o último caracter não era suficiente. Usei o trim() e era isso.

Se o exemplo acima pode ser solucionado com uma variável a mais, descobri diversas outras situações onde acontecia o contrário: a linguagem não permitia criar uma variável temporária, porque ela impedia de continuar usando a original até a temporária sair de escopo. Um exemplo disso está na função remover_contato(), onde eu queria fazer contatos[i].nome em passos separados (primeiro obter contatos[i], guardar numa variável, e depois obter o nome), mas para isso precisei criar um bloco na condição do if:

fn remover_contato(contatos: &mut Vec) {
    let nome_procurado = le_string("Nome a remover: ");
    for i in 0 .. contatos.len() { // não usei o for simples por causa da remoção
        if { // este bloco{} é necessário só por causa do escopo das variáveis
            let contato = &contatos[i];
            let nome_tmp = &contato.nome;
            nome_tmp == &nome_procurado
        } {
            contatos.swap_remove(i);
            return;
        }
    }
}

O bloco na condição do if serve para delimitar o escopo de contato e nome_tmp. Se eu não o utilizasse essas variáveis viveriam por todo o bloco do loop e eu não poderia fazer o swap_remove mais abaixo. Claro que neste caso não é nenhum problema escrever contato[i].nome numa expressão só, mas eu queria separar os pedaços para entender melhor a linguagem, e para casos onde essa separação de tarefas viesse a ser realmente necessária. Note também que a cada variável temporária usada aparecem uns &, pois temos que guardar os valores temporários por ponteiro, porque o default da linguagem é mover os valores, inutilizando a variável anterior. Esse default é útil para returns, e deve haver alguns outros casos também, já que li superficialmente na lista de discussão que moves explícitos tinham sido experimentados e deixavam a linguagem mais verbosa, com “move” pra tudo quanto é lado.

Uma curiosidade é que justamente enquanto testava esses casos, apareciam artigos na internet sobre esse mesmíssimo problema de borrows e como estão planejando solucioná-lo. Vejam estes links:

http://smallcultfollowing.com/babysteps/blog/2016/04/27/non-lexical-lifetimes-introduction/

http://smallcultfollowing.com/babysteps/blog/2016/05/04/non-lexical-lifetimes-based-on-liveness/

http://blog.rust-lang.org/2016/04/19/MIR.html

Conclusão

Neste pequeno programa pude conhecer apenas um pedaço da linguagem, mas já é muito mais conhecimento do que poderia obter apenas lendo sobre ela. Em geral achei a linguagem bastante amigável para sua categoria. Não é fácil como passar de Ruby para Python ou de C# para Java. Precisa voltar a pensar como em C++, com diversos tipos de referências, ponteiros, ponteiros para ponteiros e valores imediatos (quero dizer, aqueles tipos de objetos que ficam direto na pilha de chamadas ou são inseridos diretamente na struct em que estão contidos). Mas mesmo assim deve ser bem menos trabalhoso do que aprender as melhores práticas de como retornar strings de tamanho desconhecido em C sem estourar buffers nem deixar vazar memória.

Anúncios

Ridindaĵoj – Reformemuloj

Ĉiu esperantisto jam pensis/legis/debatis pri reformo, ĉu ne? Kaj ĉiuj aliaj timas pro atenco kontraŭ la stabileco de la fundamento. Tamen, kie estas esperantistoj, tie la temo reaperas.

Se ne eblas eviti proponojn de reformoj de Esperanto, oni povas amuziĝi per ili. Eble el ridindaĵoj aperos bona ideo. Aŭ eble la ridado fosos la tombon de iuj reformoj…

Kiel ĉiuj ni scias, Esperanto havas la finaĵon -n, por marki vortojn en la akuzativa kazo. Kaj tiu finaĵo estas ofte forgesata kaj misuzata de multaj homoj. Tamen, aliaj homoj (aŭ, nelogike, la samaj) tiel enamiĝis de akuzativo, ke ili trovis neakcepteble ke kelkaj frazoj ne havas la finaĵon -n ie ajn, malgraŭ la transitiveco de la verbo.

La ne-esto de la finaĵo -n povas okazi pro pluraj kialoj:

  1. La objekto ne estas Esperanta vorto.
  2. La objekto ne akceptas la finaĵon (iom, multe, pli, ambaŭ, unu, ktp.)
  3. La objekto estas subfrazo, titolo de verkaĵo, citaĵo, ktp.

Do, por solvi la situacion 1 (kaj foje 2), ili proponis novan prepozicion: na. La fakto ke la situacioj 3 ankaŭ ekzistas kaj kreos ne-necesajn kaj strangajn esprim-manierojn, kiel “na ke” kaj “na na” ne malhelpu reformemulojn.

Tamen, oni forgesis ke ekzistas alia grava finaĵo en Esperanto, la pluralo: -j. Do, kiel oni faru se oni volas diri ke oni legis du librojn de Harry Potter sen uzi la vorton “librojn”, kiu estas la nura ero kiu subtenas la finaĵon -jn?

Pro tiu gravega kaj neakceptebla manko de la lingvo, kompreneble oni devas uzi novan prepozicion: ja. La fakto ke tiu vorto jam ekzistas ne malhelpu reformemulojn.

La frazo do fariĝas: Mi legis ja na du Harry Potter. Tio estas multe pli logika! La fakto ke tiu ja estas nekomprenebla de ĉiuj jam ekzistantaj esperantistoj ne malhelpu reformemulojn.

Tamen, oni forgesis ke ekzistas alia grava frazrolo, krom la objekto: la subjekto, kaj ĝi ne estas markata.

Pro tiu gravega kaj neakceptebla manko de la lingvo, kompreneble oni devas uzi novan prepozicion: ka.

Estas tute evidente, ke “na” ne estas sufiĉa. Ofte okazas, ke la subjekto de frazo ne estas klare indikata:

1. Ĉu vi volas tiun libron, aŭ tiun ĉi?
Ambaŭ ni volas.

2. Ĉu vi volas libron, aŭ ŝi volas ĝin?
Ambaŭ ni volas.

Estas tute klare, ke “na” ne povas helpi tie. Do mi proponas novan prepozicion, “ka”, por indiki la _subjekton_ de frazo:

1. Ĉu vi volas tiun libron, aŭ tiun ĉi?
Na ambaŭ ka ni volas.

2. Ĉu vi volas libron, aŭ ŝi volas ĝin?
Ka ambaŭ ni volas.

Ka mi rekomendas, ke ka tiu nova prepozicio, ka kiu klare estos tre utila, estu ekde nun amplekse uzata. Ka tio certe evitos na multaj problemoj pri miskomunikado.

— de Vítor De Araújo

La fakto ke ka estas nekomprenebla de ĉiuj jam ekzistantaj esperantistoj ne malhelpu reformemulojn.

Tamen, oni forgesis ke ekzistas alia grava frazrolo: la predikativo.

Pro tiu gravega kaj neakceptebla manko de la lingvo, kompreneble oni devas uzi novan prepozicion: pa.

Ka mi rekomendas na, ke ka tiu nova prepozicio, ka kiu klare estos pa tre
utila, estu ekde nun pa amplekse uzata. Ka tio certe evitos ja na multa
problemo pri miskomunikado. Kaj ka Esperanto fariĝos pa multe pli klara lingvo.

Li farbos la blankajn domojn flavaj
fariĝos pa:
Ka li farbos ja na la blanka domo ja pa flava.

Ka tio ŝajnas al mi pa tre logika reform-propono. La fakto ke pa estas nekomprenebla de ĉiuj jam ekzistantaj esperantistoj ne malhelpu reformemulojn.

Se ka vi volas ja na pli da ja informo, bonvolu komenti tie ĉi. Ka mi imagas na, ke ka vi ĉiuj amos ja na ĉi tiu propono.

La fakto ke la rezulto kun ĉiuj proponoj kunaj aspektas kiel tute nova lingvo ne malhelpu reformemulojn, ĉu ne? Kiun problemon povus krei kelketaj novaj prepozicioj? /ironio

Rimarko: Ĉu oni devas diri ja ka, ja na, ja paka ja, na ja, pa ja? Ho, ve!