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.

CSS

Depois de um tempo sempre caindo em sites com explicações ruins sobre CSS e sempre esquecendo tudo minutos depois (explicações ruins não se fixam na minha memória), encontrei um ou dois sites quases bons (perdi os links, mas um deles deve ser este: http://css-tricks.com/almanac/
properties/d/display/
) e decidi colecionar minhas próprias explicações, para não precisar mais me contentar com explicações mais ou menos dali em diante.

O resumo abaixo não passou por um controle de qualidade completo™ (isto é, algumas coisas eu testei e outras não), mas deve ser razoavelmente útil, nem que seja para mim mesmo. Foquei este tira-dúvidas em 3 propriedades: display, position, e white-space.

display:

block

  1. Sempre começa numa nova linha.
  2. Dá pra manipular height, line-height e as margens top e bottom.
  3. A largura (width) default é 100% da largura da página, mas dá pra especificar outro valor.

Exemplos em que é o default: <div>, <p>, <h1>, <form>, <ul>, <ol>

inline

  1. Começa na mesma linha.
  2. Não dá pra manipular height, line-height nem as margens top e bottom, essas propriedades serão ignoradas (ver inline-block abaixo para a solução).
  3. A largura (width) é o tamanho do texto ou imagem contida no elemento e não é possível mudar.

Exemplos em que é o default: <span>, <a>, <label>, <strong>, <em>, <b>, <i>.

inline-block

  1. Começam na mesma linha
  2. É possível manipular height, width, etc.

Exemplos em que é o default: <button>, <select>. Há algumas divergências quanto a <img>, <input>, <textarea>, se eles devem ser inline-block ou apenas inline por default. Ver: http://stackoverflow.com/questions/21614938/html-element-which-defaults-to-displayinline-block

position:

static

É o default.

  1. A posição depende do fluxo normal dos elementos da página: seguindo a ordem de leitura de texto, com elementos block começando novas linhas, etc.
  2. Posicionamento com CSS com top, left, right, e bottom será ignorado.
  3. A única situação onde seria necessário usar position: static seria para remover outro posicionamento que foi adicionado em outro lugar. Isso ocorre raramente.

absolute

  1. Removido do fluxo normal da página. Não afeta a posição dos outros elementos e não é afetado por eles.
  2. Posicionado nas coordenadas especificadas (por exemplo, com top e left).
  3. As coordenadas usadas no posicionamento são a posição absoluta dentro do elemento contêiner.
  4. O elemento contêiner pode ser a página como um todo ou o primeiro elemento circundante que especifique  position: relative.

relative

  1. Aceita ser posicionado com top, left, etc.
  2. As coordenadas usadas no posicionamento são relativas à posição onde o elemento estaria no fluxo normal da página. Ou seja, é possível fazer coisas como: posição_normal + 10, posição_normal – 5, etc. apenas especificando relative e 10px, -5px, etc.
  3. Usar position: relative e não setar uma posição não vai alterar a localização do elemento, mas vai permitir duas coisas:
    1. usar z-index
    2. servir como elemento contêiner de elementos com position: absolute.

fixed

  1. Semelhante ao absolute.
  2. Não se move quando é usada a barra de rolagem.

white-space:

normal

  1. Seqüências de espaços viram um espaço só.
  2. Quebras de linha só quando necessário (quando o texto atinge a margem), mesmo que o fonte tenha suas próprias quebras.
  3. Quebras de linha nos <br>
  4. É o default.

nowrap

  1. Seqüências de espaços viram um espaço só.
  2. Não adiciona quebras de linha mesmo que necessário (gera linhas mais longas que a tela).
  3. Quebras de linha nos <br>.

pre

  1. Seqüências de espaços são preservadas.
  2. Não adiciona quebras quando necessário.
  3. Quebras de linha onde existirem no fonte.
  4. Quebras de linha nos <br>.
  5. Age como a tag <pre>.

pre-line

  1. Seqüências de espaços viram um espaço só.
  2. Quebras de linha quando necessário.
  3. Quebras de linha onde existirem no fonte
  4. Quebras de linha nos <br>.

pre-wrap

  1. Seqüências de espaços são preservadas.
  2. Quebras de linha quando necessário.
  3. Quebras de linha onde existirem no fonte.
  4. Quebras de linha nos <br>.

(aliás, o CSS fez aniversário de 20 anos recentemente)

Pessoas e seus algoritmos

O tímido

ambiente = getAmbiente()
fala = escolherFala(ambiente)
if fala is not None:
    falar(fala)
exit()

O extrovertido

fala = random()
while True:
    try:
        falar(fala)
    catch AmbienteException as ex:
        ambiente = ex.getAmbiente()
        fala = escolherFala(ambiente)
    else:
        fala = random()

O chato

while True:
    fala = random()
    try:
        falar(fala)
    catch AmbienteException as ex:
        pass # ignorar exceções e continuar tagarelando

Inglês é engraçado…

Peguei meu caderno, conectei um rato nele e virei sobre a máquina. Eu entendo um pouco de artigos duros, mas atualmente trabalho mais criando artigos moles. Aprendi a programar em Básico, mas agora eu programo em Montagem e Língua presa. Ainda bem que nunca precisei programar em Caxumba. Para compilar meus artigos moles eu dou alguns comandos na concha. Meu programa está correndo corretamente, mas agora eu preciso de um motorista para interpretar a entrada do bastão da alegria num jogo que estou fazendo. O motorista deve, de preferência, consumir pouca memória carneiro para não afetar o desempenho do jogo.

É assim que soa uma conversa sobre informática para um falante nativo de inglês?

Confissões de programador

Eu reformato o código dos outros para ler e “entender melhor”. Também renomeio variáveis.

E quando eu renomeio as variáveis para algo mais legível (principalmente variáveis booleanas num if), fico pensando “eu sou um gênio”. Ex.:
if(executouActionComSucesso) { avancarTela(); } é bem melhor do que
if(avancarProximaTela) { avancarTela(); }

Se eu escrevi algo conciso e inteligente, o código é expressivo. Se foi outra pessoa que escreveu, é ilegível.

Se eu escrevi código bagunçado, é porque preferi evitar uma solução over-engineered. Mas eu deixo um comentário assumindo a culpa.

A primeira vez que eu vejo um código de outra pessoa, logo penso é “WTF!!??”. Aí eu refatoro e vejo que minha solução não funciona. Então eu reverto minhas alterações.

Por isso eu acho que um dos comentários mais úteis do mundo é: “Eu tentei do jeito simples e não funcionou por isso, isso e aquilo. Então tive que fazer desse jeito mesmo.”

Uma vez quando eu fiz um switch que não precisava de um break num dos casos, eu pensei: “Uau, finalmente um bom caso para usar esse fall through do switch”. Aí eu percebi que não estava tratando uma situação, e não dava mais para usar o fall through. Revertendo… Isso me lembra o que o Douglas Crockford fala do fall through — simplesmente evite. Certamente se eu fizesse uma linguagem de programação, não copiaria do C essa necessidade de usar break. O caso mais comum deve ser o mais fácil de usar.

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.