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.

Bullies funcionais

Você é um programador que só aprendeu Java e C e não entende o que esses pedantes de linguagens de programação funcionais ficam falando? A linguagem deles não tem loops e eles acham isso bom? As variáveis são constantes? Como dá pra programar com variáveis que não variam? E ainda por cima eles ficam rindo da sua linguagem imperativa/orientada a objetos?

Seus problemas acabaram! Chegou a explicação supersucinta sobre recursão e linguagens funcionais que vai permitir a você dizer com orgulho: “Eu sei o que significa tail recursion!” e colocar esses valentões em seu devido lugar.

Eis:

Faça uma chamada recursiva como última instrução da sua função, e você terá basicamente um goto com parâmetros! Cada loop será uma função, e todas as variáveis que mudam de valor durante iteração devem ser transformados em parâmetros dessa função.

Dá até pra traduzir de um estilo para o outro mecanicamente, sem pensar muito! As variáveis só mudam de valor nas chamadas de função, aí faz sentido aquela história de “variáveis imutáveis”.

Tirinha relacionada do xkcd:

http://xkcd.com/1270/

Outra leitura interessante:

http://prog21.dadgum.com/23.html

Atenção: este post não oferece proteção contra programadores Haskell que vivem falando de monads. Use por sua conta e risco.

Truques bobos em Java (2)

GOTO

Oficialmente, Java não tem goto. A palavra é até reservada, mas só para poder dar erro mesmo…

A alternativa dada são break e continue rotulados. Eu achava que só era possível rotular loops (afinal, break e continue são usados em loops), mas lendo a especificação (o truque da postagem anterior eu também descobri lendo a especificação) percebi que qualquer statement pode ser rotulado, transformando break praticamente em “goto pra frente”! Edit: O continue bem que poderia ser um “goto pra trás” mas na verdade dá um erro (ver comentários deste post). É possível transformar o continue num goto para trás criando um loop “só para satisfazer o compilador” assim: label: for(;;){ <...código...> if(condição) continue label; <...código...> break label; } mas aí seria muita gambiarra. Fiquemos então com o break apenas, já que a idéia era mostrar um recurso normal da linguagem mas pouco usado.

Por exemplo, se quisermos procurar por caracteres inválidos numa String e fazer um escape nela, sem precisar alocar uma nova String caso a original já seja válida, podemos fazer um loop para testar toda a String e outro para fazer o escape apenas se o primeiro sair com break.

checkString: {
     for(...) {
         if(...) break checkString;
     }
     return originalString; // checked the whole String, no problem found
}
// invalid character found, escape it and return a new String
StringBuilder builder = new StringBuilder(originalString.length() + EXTRA_BUFFER);
for(...) {
    ...
}
return builder.toString();

Outras maneiras de fazer seriam: repetir o teste fora do for para descobrir se saiu normalmente ou com break, quebrar a lógica em funções menores, colocar todo o código de escape dentro do if que faz o break (se for só um), ou simplesmente cortar fora metade do código e montar uma nova String sempre, mesmo que não haja nenhum caracter inválido. Todas são soluções aceitáveis, mas numa biblioteca eu esperaria a implementação mais eficiente, sem cópias desnecessárias.

Truques bobos em Java (1)

SWAP

Desde muito tempo atrás sempre me admirou a incapacidade da linguagem Java de permitir criar uma abstração para trocar o valor de duas variáveis. Os criadores da linguagem se dedicaram mais à orientação a objetos do que com as pequenas praticidades (eu ia dizer “pequenas coisas práticas do dia-a-dia” e aí lembrei que eu não preciso de uma função swap no dia-a-dia… Mas eu quero uma mesmo assim!). Sem passagem por referência, sem retorno de múltiplos valores, sem macros, como fazer?

Em fóruns da internet, só se ouve respostas: “não dá”, “só se você criar um objeto mutável ou um array de 1 elemento, e trocar seus conteúdos na função”, “faça à mão mesmo com uma variável temporária para não perder desempenho criando objetos” (bastante sensato!) e de vez em quando alguns truques com aritmética ou XOR. Mas eu nunca vi essa sugestão:

int returnFirst(int x, int y) {
    return x;
}
int a = 8, b = 3;
a = returnFirst(b, b = a);
System.out.println("a: " + a + ", b: " + b); // prints a: 3, b: 8

Usando a função auxiliar returnFirst (que poderia ter um nome mais sucinto, mas escolhi esse nome para fins didáticos), temos uma solução que ainda cumpre quase todos os meus requisitos:

  1. A troca de valores é feita em apenas um statement;
  2. Não é necessário declarar uma variável temporária (não polui o código do chamador);
  3. Não aloca objetos temporários;
  4. Com alguns overloads, sendo um deles com um generic <T>, funciona para qualquer tipo;
  5. A implementação da função auxiliar é trivial;
  6. Não usa truques sujos que só funcionam para números inteiros.

O único trabalho é passar os argumentos na ordem certa e não se confundir com as duas atribuições! (Edit: ignore o nome da função e simplesmente leia como se fosse a=b;b=a; tá certo que se fosse escrito assim não funcionaria e tá certo que b=a executa primeiro, mas o importante é a idéia!)

A especificação da linguagem Java (Java Language Specification, Java SE 7 Edition, seção 15.12.4.2) garante que o valor de b é avaliado e passado antes de ser sobrescrito pela atribuição b = a no segundo argumento, ao contrário de outras linguagens (pelo que me lembro, C e C++ não são nada amigáveis e não garantem ordem nenhuma de avaliação dos argumentos, só garantem que eles todos serão avaliados antes da função começar a executar, obviamente. Vou ficar devendo uma referência à especificação).

Então com nossa funçãozinha auxiliar returnFirst, o valor de b é passado para o parâmetro x, logo a seguir b é sobrescrito com o valor de a, e quando returnFirst executa, não faz nada além de retornar o valor antigo de b, que pode ser atribuído a a. Tudo isso para evitar ter mais um ponto-e-vírgula na linha e uma declaração de variável :-)

Se eu usaria isso de verdade? Talvez seja útil algum dia num caso muito específico: num algoritmo com várias trocas o código ficaria mais limpo se feito dessa forma. É legal descobrir um truque desses que não encontrei no StackOverflow!

Scalando (2)

Então, apesar de jurar para mim que eu não tinha tempo para mais nada (e apesar de achar [ter sido convencido] que o tempo que eu tinha deveria ser dedicado a alguma atividade física), resolvi me inscrever no curso Functional Programming Principles in Scala do Coursera (e apesar de ter dito que não sou tão fã do design dessa linguagem, e apesar de ter esbarrado em bugs do compilador nos primeiros minutos de uso, e apesar de não ver para que eu usaria o conhecimento do curso, nem saber o quanto vou aprender de verdade e o quanto vai ser só revisão). Depois de sair no news.ycombinator.com a notícia de que o curso estava começando, serviu como um sinal, haha.

Está ainda na primeira semana e a data limite dos primeiros exercícios está longe, então se algum (raro) leitor deste blog estiver a fim, dá tempo. Por enquanto os exercícios estão bem tranqüilos, dá pra fazer até sem assistir às aulas pra quem sabe essas coisas de fatorial ou fibonacci recursivos (nenhum desses é o exercício de verdade). Não chega a ser possível fazer “sem pensar”, mas a dificuldade está OK. É como voltar ao início da faculdade, mas com “macete” (↑↓←→ A+Start) de já ter terminado uma antes, hahaha.

Scalando

Resolvi dar mais uma chance à linguagem Scala, já que me disseram que se eu gosto de F# eu poderia muito bem gostar de Scala também. Da última vez eu tinha deixado ela de lado porque me pareceu que tinha muitas coisas implícitas (e como o import this do Python diz, “Explicit is better than implicit”), mas talvez eu tenha julgado mal né? Além do mais, eu tentei usar Scala como  um REPL pra Java, mas isso era pedir demais (tinha que lembrar que int era Int, que <T> era [T], e mais um monte de coisas)! Pra esse caso é melhor o Groovy, e usar Scala só como Scala mesmo!

Baixei a última versão, 2.10.1 e fui testar a sintaxe e os tipos básicos no REPL, enquanto também lia algumas coisas mais avançadas (que talvez eu teste mais adiante). E nisso, brincando com coisas básicas, BAM! Crash no compilador. Não, não foi só erro de compilação, foi erro interno do compilador mesmo.

Poxa, eu só queria saber se aceitava {} na condição do while, assim como é permitido no for…

Eis o código:

while {i < 10} { i += 1 }

(Ah, sim! Scala obviamente tem aquele engasgo característico da JVM… Você dá Enter, conta de 0 a ¼ e só aí o negócio executa — deve ser isso que me dá a impressão que Java é mais lento que Python, embora na categoria “mastigação de dados” o Java na verdade seja bem mais rápido)

Java não é tão ruim assim…

Java é uma linguagem grande, famosa, e muito usada. Mas não é, digamos, “charmosa”. Não só eu, mas muita gente reclama que ela é inflexível, sem-graça, e demora a adotar recursos que as concorrentes têm faz tempo (em termos de recursos da linguagem, o C# da Microsoft evoluiu muito mais rápido).

Dependendo do gosto do programador, uma linguagem charmosa seria Python (eu gosto, é quase um pseudocódigo executável), alguma variante de Lisp (os fãs são dedicados, mas eu nunca consegui me adaptar), Ruby (superficialmente lembra Python, é muito flexível, mas sintaticamente é mais complexa), Lua (minimalista e elegante, quase um pseudocódigo executável, como Python), ou até C (reina tranqüilo no seu nicho de infraestrutura, bom para quem gosta do “baixo nível”), C++ (ótimo poder de abstração sem perder as capacidades do C).

Mas aí eu parei para pensar mais friamente: exceto por alguma inveja aqui e ali do C#, Java tem suas qualidades.

Comparando com C e C++:

A tipagem estática e sintaxe simples permite navegação e refatoração fáceis com ferramentas de desenvolvimento. Compare com C++, onde ferramentas de “autocompletar” facilmente falh(av)am porque não entendem as milhares de macros e templates a serem processados nos headers.

O ambiente gerenciado permite reflexão, serialização e stack-traces bem úteis. Já em C e C++, bah… Nessas linguagens, se tiver um core dump para descobrir por que o programa morreu, o programador já acha excelente! Java tem strings Unicode também; em C/C++ tem que correr atrás de alguma lib pra isso.

Em Java até precisa escrever o nome da classe 2 vezes ao instanciá-la, mas diferente de C, C++, e Objective-C, não é necessário ter 2 cópias da assinatura de cada função (nome e parâmetros) uma no cabeçalho e outra na implementação (e a instanciação em Obj-C é pior, pelo que li: Classe* obj = [[Classe alloc] init];). Depois de um tempo longe do C e C++, tinha até esquecido essa incomodação da replicação entre .h e .c.

Em Java dá para deixar outras pessoas mexerem no seu código sem (muito) medo de estragarem seu cuidadoso gerenciamento de memória e ponteiros, depurado com muito suor (entretanto, talvez esqueçam de fechar a conexão com o banco…).

Comparando com as linguagens dinâmicas (Python, Ruby, etc.):

Java é pesado e consome uma enorme quantidade de memória, mas apesar disso, tem o potencial de ser mais rápido que qualquer linguagem dinâmica.

Java é estável, a linguagem não muda radicalmente a cada ano (nem a cada dois anos, talvez mude algo depois de três anos). Dá para começar e terminar seu projeto tranqüilo sem chegar no fim com aquela sensação de estar usando uma versão obsoleta da linguagem.

A questão de reflexão e bibliotecas deixa de ser uma vantagem do Java (como era em relação ao C++), mas em compensação, em Java é muito mais fácil criar interfaces razoáveis e autodocumentadas (por causa das declarações de tipos) entre módulos diferentes da aplicação. Em Python, é um vale tudo: se algum espertinho ou distraído resolver adicionar um atributo de objeto chamado tmp35 no if mais escondido do arquivo A e acessá-lo num tratador de exceção que quase nunca é executado no arquivo B, a linguagem vai aceitar; o atributo vai existir ou não dependendo do if, e o tratador de exceção pode ter um comportamento diferente dependendo disso.

Você queria ter uma boa mensagem de erro mas o próprio tratador de exceção pode jogar uma exceção por causa de uma variável não encontrada, enquanto que em Java um problema desses não passaria do estágio de compilação. Em Java, muito mais problemas são detectados logo na compilação, ou pelo próprio editor que já compila o programa em segundo plano. Geralmente dizem que basta usar testes unitários com as linguagens dinâmicas, mas não sei… Precisaria de muito mais testes (escrever os testes é complicado!), e não daria para esquecer de testar os casos mais excepcionais, já que vários tipos de erro de digitação só são detectados quando o programa executa tal linha. Ao fazer uma grande refatoração numa linguagem com tipos mais estritos, pelo menos temos uma garantia que não deixamos nenhum pedaço do código horrivelmente inconsistente, né?

Comparando com outras linguagens:

Apesar de verbosa, a linguagem Java vem do C++, com práticos atalhos como i++; i += 2; a = b = c = 0; declaração de variáveis em qualquer local da função; operador ternário ?:; e ganhou um for-each bem antes do C++. Imaginem se Java viesse do Pascal, com variáveis declaradas só no começo das funções, i := i + 1; e os horríveis begin e end, que embora teoricamente sejam necessários “apenas para instruções compostas”, acabam sendo necessários quase sempre, porque a linguagem não permite fazer muita coisa por linha (nada de v[i++] = w[j++] = x[k++]).