A Linguagem Nim: Introdução

Antes de começar a falar da linguagem em si, acho interessante falar sobre como acabei descobrindo-a e, especialmente, como resolvi encarar esse trabalho de pastar feito um jumento que é aprender uma nova linguagem de programação.

Pastar

Sim, pastar. Sofrer e ficar de saco cheio.

Há uns dias comentava com colegas que, no fim das contas, a dificuldade que nós, adultos, geralmente temos para aprender novas linguagens — idiomas “de verdade” ou linguagens de programação — é, mui provavelmente, menos relacionada à capacidade de nossos cérebros absorverem as coisas novas e muito mais ao fato de que estamos tão acostumados a simplesmente já saber que somos impacientes demais para nos sujeitar ao processo lento de abordar algo sabendo pouco ou nada a respeito.

Estamos tão acostumados a saber dizer “oi”, “tchau” e “hoje acordei com uma dor esquisita no calcanhar” que é frustrante demais passar meses aprendendo um novo idioma e, ainda assim, não fazer a menor ideia de como é que se pede dez pães na padaria.

Me give 10 pieces of françois pane, s’il vous plâit? Efharisto!

E eu percebo que o mesmo acontece comigo com linguagens de programação. Especialmente essas menos “badaladas”. Você lê dois ou três tutoriais da linguagem e acha que já entendeu tudo, mas aí precisa abrir um arquivo, ler uma dúzia de caracteres e transformar tudo em números inteiros e, pronto, você percebe que dominar uma nova linguagem ainda requer muitas horas de manuseio da mesma para aprender todos aqueles milhares de macetes que você já sabia do coração na linguagem anterior.

Python

Eu comecei minha carreira desenvolvendo em linguagem C, mas rapidamente comecei a tocar projetos pessoais, geralmente, em Python.

Minha história com Python é até curiosa: eu ouvia falar da linguagem e tinha curiosidade de conhecê-la (lá pelos idos de 2004). Comecei a ler um tutorial (Dive into Python) e quando percebi que as funções eram definidas usando-se a palavra-chave def eu fiquei absolutamente chocado e não consegui prosseguir.

Pois é. Eu era bem jovem…

Eventualmente acabei superando a barreira da “estranheza” e comecei a me afeiçoar pela linguagem.

A linguagem perfeita?

É claro que sempre vale aquela máxima: “use a ferramenta certa para o trabalho certo”. Ninguém em sã consciência apregoaria que deve-se usar martelos para tudo. Mas naquele grande espaço de casos de uso em que “uma linguagem genérica o bastante” cabe bem, eu ouso dizer que Python, que acompanho desde a versão 2.3, é, basicamente, a linguagem perfeita. Ao ponto de eu quase considerar Python uma coisa ruim: uma vez que você aprende, tudo o mais parece pior. Você começa a ver construções em outras linguagens e logo se vê pensando “mas em Python é tão mais fácil…”.

Exceto, claro, se você tem previamente um universo de fatos e precisa fazer análises de verdades. Aí Prolog é mais fácil.

Você entendeu o que eu disse, certo?

E estou dando muita ênfase à linguagem em si. Há de se falar sobre o ecossistema, também, que é bem rico.

Há solução meio que pra tudo e são razoavelmente raros os casos em que Python absolutamente não dá conta do recado —ao menos nesse mundo das “linguagens genéricas o bastante”, claro.

(Mas se você precisa programar o firmware de um PIC18, por favor, não invente moda e programe em C — como todo mundo faz.)

Análise subjetiva

Vivemos num mundo em que determinadas pessoas acham código Perl algo agradável à vista, o que me obriga a comentar que, sim, minha análise sobre a qualidade da linguagem Python é afetada por fatores subjetivos. Eu tenho, sim, preferências bem claras com relação a sintaxe de certos construtos comuns.

Por exemplo: o for . Em Python, iteramos sobre uma sequencia assim:

for item in sequence:
    print(item)

Eu, particularmente, acho isso lindo. Pois, afinal de contas, como é que eu diria, em linguagem natural, “para cada item nessa sequencia”? Praticamente desse jeito, aí (provavelmente falando “for each”, claro, mas aí é uma outra discussão…).

Já em C, teríamos:

for (i=0; i ᐸ sequence_length; i++) {
    printf("%s", sequence[i]);
}

Há outras maneiras de se escrever isso, provavelmente mais seguras (quem programa em C sabe do que estou falando), mas eu realmente queria mostrar um caso de for , porque eu gosto dessa sintaxe. É concisa e facilita a vida do programador em 90% dos casos em que é necessária.

Mas há jeitos horrendos de se implementar isso. Pelo menos de acordo com o meu gosto. Um bom exemplo é a linguagem Ruby:

sequence.each { |item|
    puts item
}

(Eu sei que um for igual ao do Python é possível, mas é algo que fará os outros programadores Ruby pararem seus toca-discos imediatamente, baixarem as telas de seus MacBooks e olharem feio para você enquanto terminam seus suquinhos de soja.)

Eu detesto essa sintaxe porque ela é anti-natural e porque conspurca o código com uma porção de “rabiscos” desnecessários. E esse último ponto é bem importante, porque para um programador Python experiente, não há muita diferença entre código Ruby e código Perl. Ambos são cheios de ruído e difíceis de entender.

Ninguém chega no mercado, saca a lista de compras e pensa “lista de compras → cada:”. O natural é “para cada item na lista de compras”. Na tentativa de ser ~ coerente ~ (e eu uso aspas irônicas com muita razão no caso do Ruby) a linguagem se degenera numa sintaxe que dá tapinhas nas costas dos programadores que se acham “espertinhos” por conseguirem entender uma sintaxe feia e confusa.

E se orgulhar por fazer as coisas de forma mais complicada do que o necessário é coisa de piá*.

*para quem não é do Paraná: piá = guri, moleque, menino, mancebo.

Python em produção

Provavelmente eu já havia dado uma olhada nessa tal de “Nim” há alguns anos, inclusive achando a sintaxe bem interessante, mas algum motivo acabou me fazendo ficar longe. Provavelmente foi a soma do fato de Nim “transpilar” para C (além de C++ e Javascript, curiosamente) e de eu, na época, ter decidido me especializar mais profundamente em Python ao invés de ficar aprendendo novas linguagens.

É bem possível, inclusive, que eu tenha encontrado Nim durante uma busca por alguma alternativa sã à escrita de Javascript, e essa associação entre as ideias (Javascript e Nim) acabou me fazendo deixar a linguagem de lado. (É capaz de eu ter achado CoffeeScript mais interessante, na época, para o problema em mãos. Veja só…)

Enfim.

Acontece que nas últimas semanas A Linguagem Perfeita™ começou a encher o meu saco quando comecei a sair, por necessidades trabalhísticas, do lindo reino encantado que é o ambiente controlado do desenvolvimento local e tive que botar muito código Python em muitas configurações diferentes em muitas máquinas distintas.

Em um serviço tem que rodar Ubuntu 16.04, mas o código só roda em Python3.6, mas essa versão do Ubuntu só tem o Python3.5, aí o pyenv salva o dia. Ufa.

Em outro tem que usar a biblioteca GDAL, mas também estamos usando pyenv , então não vamos usar os pacotes python3-*do sistema mas aí exportar as variáveis certas e instalar o pacote GDAL passando os parâmetros corretos para o pip resolve. Ufa.

Eu outro tem tanto Ubuntu 16.04 quando pyenv quanto GDAL quanto uns requisitos de pacotes meio diferentões, mas aí o Docker resolve. Ufa.

Mas então cada build com alterações no Dockerfile que precisa rodar uma versão antiga do Ubuntu instala um milhão de pacotes (Ubuntu instala o exim4 se você quer o postgres rodando. CALCULE!) demora eras e mais eras para instalar as dependências de pacotes do pip e já é tudo muito chato e um belo dia um mantenedor de PPA resolve ficar indignado e tira do ar todos os pacotes que mantinha e agora o build que funcionava normalmente começa a falhar e você não faz ideia do motivo e só quer tacar fogo nisso tudo e voltar a programar em C e compilar tudo estático e gerar um standalone que roda em qualquer x86_64 com Linux.

Ufa!

(Essa linha é uma forma de prevenção contra gente afobada: antes de vir comentar “ain purque vc naum usa o Alpine ki é muitu mais lévi” saiba que não, nessas ocasiões específicas o Alpine não era uma opção viável.)

Back to C

E eu realmente comecei a considerar que seria muito bom conseguir simplesmente criar um programa standalone que já tivesse tudo incluso, mesmo que fosse um binário de 250MiB. Contanto que me livrasse desse pesadelo, eu já estaria bem feliz.

“Escrito em”

Além disso, de certa forma eu vejo que as “linguagens de altíssimo nível” acabam formando “guetos” próprios: eu mesmo tenho provavelmente nenhum programa escrito em Ruby rodando na minha máquina. Ou Perl, que eu ativamente evito. Como minha linguagem principal é Python, toda vez que eu vejo “written in Python” na descrição de um programa eu acabo me animando. Na prática, meio que por motivo nenhum, já que raramente eu “conserto” um programa (geralmente eles simplesmente já funcionam) e, se quiser ler código, eu posso simplesmente clonar o repositório e lê-lo.

Logo, essa “etiqueta” do “written in X” tem um certo “efeito social” que, à parte de certas especificidades de cada linguagem e sistema de distribuição, acaba sendo gratuito. “Written in Go” e “written in Rust”, na prática, não tem diferença alguma. Mas quem programa em Go vai querer instalar o que é feito em Go. Por quê? A não ser que trate-se daquela minoria que realmente vai fuçar o código fonte e corrigir algo ou propor uma melhoria, por motivo algum.

E eu tenho a impressão de que algumas linguagens são mais neutras nesse aspecto, ao ponto de dispensarem a tal etiqueta. E a principal delas, no caso, é justamente a linguagem C.

Clang!

C é a “linguagem default”, de certa forma. Mesmo quem nunca escreveu uma linha em tal linguagem aceita bem programas escritos nela. Afinal, estamos cercados por eles, não?

Entretanto, C ainda é meramente uma “assembly language portável”: trata-se de uma maneira padronizada de se falar com a máquina diretamente, ao ponto de que pouquíssimas abstrações são implementadas pela linguagem em si. C não tem nem mesmo uma “standard library” nos mesmo moldes das que nossa geração “Nutella” está acostumada, como Python, Ruby ou mesmo Java. C é mais baseada em padrões do que implementações.

O que temos de mais próximo de uma “biblioteca padrão”, hoje, seria a glibc . Outras implementações tem ganhado tração, ultimamente, especialmente a musl-libc (estou usando um sistema completamente baseado nela, agora), mas a conversa sempre gira em torno de comparações com a biblioteca implementada pelo povo do GNU.

Mas mesmo que tenhamos várias abstrações implementadas, ainda assim escrever um programa em C é estar com 99% da execução do programa sob controle. A libc esconde, por exemplo, as syscalls , mas ainda assim você sabe muito bem que elas estão lá e trabalha, de certa forma, com consciência a respeito delas. Você chama a famigerada malloc mas sabe que lá por trás dos panos a libc está chamando uma syscall que chuta o “break” mais pra frente quando necessário.

E, ademais, nada te impede de chamar as syscalls por conta própria. Você nem é obrigado a usar uma libc , inclusive.

Em resumo, ao programar em C, você tem que fazer praticamente tudo. Tem que alocar memória, tem que cuidar dos tipos e eventuais coerções, tem que liberar a memória alocada, tem que instanciar e inicializar as structs devidamente, et cetera, et cetera. E ao mesmo tempo que isso é sinônimo de controle, na maioria das vezes (especialmente ao desenvolver-se aplicações para o desktop) acaba também significando um montão de trabalho completamente desnecessário.

Você até começa seu novo projeto felizão pensando em fazer tudo direitinho, tudo redondinho, otimizado ao máximo, mas lá pelas tantas você fica de saco cheio e percebe que está passando tempo demais focando em detalhes de compilação ao invés de pensar na implementação em si. E isso é especialmente verdade quando precisa usar várias bibliotecas de terceiros, cada uma requerendo a devida configuração (por meio de uma cadeia de structs própria, geralmente) e inicialização (chamando funções passando ponteiros).

Isso é ruim? Não necessariamente. Mas ao programar “para desktop” (eu uso a expressão em contraste com outras modalidades, como programação de firmware, por exemplo, em que tudo é muito diferente), você percebe que gasta tempo demais em repetições e mais repetições ou idiossincrasias que poderiam ser resolvidas de maneira mais simples.

Por isso, ao invés de já ir metendo a mão no C novamente, comecei a procurar uma linguagem que (1) permitisse integração seamless com bibliotecas em C, (2) fosse compilada de maneira eficiente e (3) implementasse um mínimo de “facilidades”, (4) ao mesmo tempo que não escondesse demais a implementação das coisas.

Go e Rust

É claro que acabei voltando a dar uma olhada em Go e Rust, também.

Go é uma linguagem criada por uma turma que eu respeito. Mas tenho cá meus próprios motivos para não ter absolutamente nenhum interesse na linguagem.

Começa pela sintaxe: eu não amo e nem odeio. Pelo contrário. Nunca vi nada mais chocho: é apática, árida, insípida. Não empolga e nem causa aversão.

Talvez essa mornidão acabe sendo parte da força da linguagem: não é muito bom nem muito ruim: é médio pra todo mundo.

Rust sempre me pareceu mais interessante, embora seja bem importante ressaltar que cada uma (Go e Rust) tem focos distintos. Mas sempre que vejo algo mais aprofundado sobre Rust (e o mesmo serve para Go) eu tenho a sensação de que não é o jeito que eu gostaria que as coisas fossem feitas.

No geral, são linguagens interessantes, com inovações que contribuem para o cenário em geral das linguagens de programação, não somente em questões de sintaxe, mas em ecossistema, implementações (a história do garbage collector do Go é bem interessante), comunidade, etc. Mas nunca me cativaram. Se for para sair do C, que seja para tentar algo minimamente empolgante…

Zig

Nem sei dizer como é que acabei encontrando Zig, mas olhando por cima já me pareceu uma ideia muito interessante. Dentre todas as abordagens que visam, eventualmente, suplantar C, certamente Zig tem a melhor de todas: Zig é também um compilador de C.

Ou seja: praticamente todo código C pode ser “trazido para dentro” de um projeto Zig e o compilador cuidará de integrar tudo adequadamente. Da parte do código escrito com sintaxe Zig, basta marcar as funções ou variáveis com extern que estas já ficam disponíveis também para código escrito em C, tudo C ABI compatible.

Além disso, Zig já inclui as libc (literalmente: faz parte do pacote) e consegue trabalhar muito bem criando programas para alvos em que não há biblioteca de C disponível, seja criando binários “crus” mesmo ou incluindo uma biblioteca de C com ligação estática gerando um standalone grandalhão.

E lá fui eu começar a aprender Zig.

Entre outras coisas, Zig também tem seu próprio build system, o que é bem legal. Mas foi justamente minha tentativa de usá-lo que acabou me mostrando algumas deficiências muito sérias.

E aqui é importante notar que Zig ainda não chegou na versão 1.0 (este artigo foi escrito em dezembro de 2019). Ou seja: ninguém estava me enganando: a linguagem e o ecossistema em geral podem apresentar problemas dos mais diversos e isso é bem claro.

Minha dificuldade foi uma função que esperava uma “lista de listas” (ou melhor, um array de arrays ). Mas não era qualquer lista. Era isso aqui:

[]const []const u8

Na prática, é uma lista de strings. E, na prática, eu absolutamente não consegui passar um parâmetro desse tipo. O compilador insistentemente transformava meus valores em algo como [][8:0]u8 . E não havia coerção nesse mundo que me deixasse fazer as coisas funcionarem.

O que acontecia, no fim das contas, era que a sintaxe de array confundia-se, nesse caso, com a sintaxe de slice .

Mas, ora, ora, ora… em C era mais fácil…

const char **

Pronto. Taí. Uma lista de strings.

E, afinal, pra que diabos implementar array e slice ? Zig ainda trabalha com alocadores de memória manuais. Tem sentido esconder os ponteiros para implementar essas abstrações que nem ajudam tanto assim?

E por essas alturas minha empolgação com Zig deu uma murchada.

Mas não me entenda mal: eu ainda acho Zig uma linguagem magnífica e acredito que o futuro do desenvolvimento de sistemas (“system programming”) será justamente Zig — ou algo parecido.

(E não Rust, curiosamente. Linguagens desenhadas por comitê geram camelos de qualidades variadas, mas não bons cavalos, afinal.)

Finalmente, Nim

Depois de passar praticamente o fim de semana todo lidando em Zig e agora cansado e meio desacorçoado, acabei voltando a dar uma olhada em Nim. Afinal, “Nim compila para C”, o que provavelmente facilitaria a comunicação com bibliotecas em C.

E tive uma enorme surpresa conforme fui me aprofundando mais no estudo da linguagem. Em resumo, encontrei muitas boas decisões. Nim é uma linguagem que, a meu ver, faz praticamente tudo muito certo.

Okay, nada se compara à elegância do C na definição de variáveis:

int x = 10;   // ← C

Confesso que não aprecio muito a indicação de tipo com : …

var x: int = 10   # ← Nim

É a diferença entre dizer “o inteiro X tem valor 10” e “a variável X, que é um inteiro, tem valor X”. C ainda é muito mais limpo e direto ao ponto, embora eu tenha que admitir que é legal saber que diferenciar entre variáveis cujo valor será alterado e cujo valor não o será ajuda o compilador a gerar código mais eficiente.

A sintaxe de Nim traz muita coisa de Python, inclusive o denteamento como forma de identificar blocos (embora a linguagem tolere brackets, também). O código é geralmente bem limpo, sem excesso de símbolos.

E isso toca um pouco no que eu digo sobre “decisões corretas”. Zig, por exemplo, decidiu implementar o “for mágico”. E pior: mágico e feio:

for (sequence) |item| {debug.warn(item)}

Bizarro. Go também tomou esse caminho terrível com relação ao controle de laços, em que tudo é um for .

Em Nim, tudo certo:

for item in sequence:
  echo item

Os parêntesis nas chamadas de funções são opcionais, e há alguns casos, como o echo , em que eu vejo que isso é uma vantagem.

Existe o garbage collector e ele é implementado com contagem de referências, como no Python. Acho excelente.

A linguagem oferece uma implementação de Orientação a Objetos que, curiosamente, é ocasional: a linguagem suporta OOP porque a forma com foi construída acaba tornando trivial trabalhar com isso, embora não haja na linguagem em si nenhum construto do tipo class , por exemplo.

Entre C e Python

Nim é uma linguagem que preenche muito bem um espaço que fica entre C e Python: você quer gerar um binário executável razoavelmente bem otimizado, mas não quer se preocupar com tudo. Você consegue perceber facilmente a alocação de memória sendo feita, por exemplo, mas não precisa fazê-la manualmente, tampouco efetuar a liberação. Você ainda lida com ponteiros em um lugar ou outro (o que é um bom sinal), mas eles não são tão prevalentes (o que também é um bom sinal).

No geral, as abstrações são “médias”: a linguagem não exagera em implementações. Ao invés disso, ferramentas foram criadas para que as abstrações possam ser implementadas sem danos à linguagem em si: o desenvolvedor pode criar templates (que são similares aos #define do C) e macros (que trabalham diretamente na árvore sintática, mas com escopo devidamente limitado).

Nim não substitui C. O código gerado é bem otimizado, mas ainda tem sua própria “gordura” (nada mais justo). Se eu precisar escrever firmware para um microcontrolador, por exemplo, não pensaria em fazê-lo usando Nim.

E Nim não substitui Python. Python é uma linguagem com alcance de aplicação muito amplo e posso dizer com alguma segurança que para qualquer aplicação em que (1) distribuição e (2) desempenho não sejam grandes preocupações, ainda é mais interessante usar Python do que qualquer outra linguagem.