/ nim-programmin

The zen of Nim #3

Hoje vamos tratar alguns tópicos para darmos introdução ao desenvolvimento guiado a exemplos práticos, porém, ainda sim é necessário a carga teórica de como funciona o compilador de Nim, juntamente ao funcionamento de um binário gerado pelo mesmo, para seguir o mesmo padrão de postagem dos outros episódios da série, vou tentar sempre escrever de forma simples e amigável.

1.1 Compilação

Como já vimos antes, o compilador passa sempre pelo processo de gerar seu binário em um código-fonte C, que em seguida alimenta o compilador da linguagem com as devidas flags para otimização. Vamos abordar os benefícios dessa prática de compilação, se você conhece GObjects, como temos presente na linguagem Vala, vai notar uma certa similaridade entre eles, porém seu funcionamento é completamente diferente.

A linguagem de programação C é bem concisa e madura, porém como sabemos não é nem de longe uma boa escolha para o desenvolvimento de qualquer tipo de aplicação em pleno século XXI, por ser uma linguagem de programação de sistemas presente no mercado a mais de 40 anos, podemos considerar ela como uma linguagem muito portável, porém com binários totalmente fixos para determinada plataforma.

Outro motivo legal para se usar C, é o fato de que seu binário pode ter objetivado para geração em diversas arquiteturas, e por esse motivo, por diversos sistemas operacionais, como x86, ARM e AMD64. Caso você queira se aprofundar mais na linguagem, recomendo seguir o guia presente na própria documentação original da linguagem, mas foque no compilador clang para maior entendimento de uma escrita moderna do idioma.

Nim aproveita todos os features bons da linguagem, assim como descarta tudo aquilo que é ruim, dessa forma temos uma forte base na sua generalização e eficiência. Além do mais, a compilação para C, facilita o uso de qualquer biblioteca C/C++ existentes, basta escrever alguma solução de wrapper para seu código, vamos ver alguns exemplos disso mais tarde.

Outras linguagens de programação que conversam muito bem, é Ruby e Rust!

Pelo fato de Nim visar muito a facilidade da implementação, até mesmo com bibliotecas mais arcaicas do C, você pode utilizar facilmente um conjunto de módulos presentes na ferramenta c2nim, basicamente, ela tem o poder necessário para converter os cabeçalhos C e C++ para código Nim, vamos também estudar isso mais pra frente.

Como temos no .NET Framework, um binário escrito em alguma implementação do mesmo, pode ser compatível com todas as linguagens contemplada, com Nim, você pode consumir nativamente o código de alguma biblioteca em C e C++, algo que não é muito bem empregado em outras linguagens, como o próprio Python (ou até mesmo que a implementação do JIT do próprio .NET).

Outro recurso de extremo poder, totalmente fornecido pelo compilador de Nim, escrito em Nim, é que seu código pode ser compilado para geração de binários em outras linguagens além de C e C++, como Objective-C e JavaScript, então sim! Você pode desenvolver nativamente para iOS e Mac utilizando Nim, e para Android, você pode usar algo como o NDK dentro do backend de aplicações para o sistema escritas em C++.

Falando agora sobre o lado client-side de aplicações web, você pode consumir o compilador para gerar um compilado de JavaScript, então sim, você também pode escrever aplicações nativa para Web, assim como suporte para frameworks e ambientes de desenvolvimento para JavaScript, como Electron ou o próprio Apache Cordova.

Algo que você vai ficar maravilhado ao escrever com Nim, é o tempo de compilação excelente que a linguagem produz, uma velocidade que chega a ficar em alguns cenários de igual para igual com Go! Isso foge um pouco do que a maioria de novos desenvolvedores da linguagem podem imaginar, pois gerar um binário intermediário, para depois seu executável é normalmente um processo custoso.

Outro recurso muito bem utilizável dentro do compilador Nim, é que ele cria um arquivo de cache para o processo compilatório, um fonte aqui na minha maquina, não muito complexo, mas com aproximadamente 10K linhas de código, demora em torno de 10 ~ 12 segundos para a primeira compilação, e algo quase instantâneo para a compilação com o arquivo de cache, menos de 2 segundos!

Fica tranquilo, vamos aprofundar nisso na prática, compreendendo melhor sobre o funcionamento do compilador quando começarmos a desenvolver aplicações que necessitam de parâmetros específicos para o processo de compilação.

1.2 Gerenciamento de memória

Bom, como sabemos, tudo aquilo que é volátil dentro de algum programa de computador de registradores, precisa ser armazenado em algum local, lá em outras linguagens como C e C++, o gerenciamento da memória precisa ser feito de maneira manual, agora no Nim, isso é gerenciável de maneira totalmente automática, mas também permite que você escreva suas implementações para seu próprio gerenciamento.

Esse conceito de automatização, que realiza a reciclagem de todos os recursos que você utiliza, e o mais importante, o que você não utiliza, normalmente isso é chamado de coletor de lixo, que também recebe essa mesma nomenclatura em Nim, porém diferente de linguagens como C# e Java, você não se limita somente a instâncias de controle para somente um coletor.

Isto é, dentro da linguagem temos vários coletores de lixo, que você pode definir sem nenhum tipo de preocupação diretamente no compilador, com a flag --gc:<nome>, como disse no paragrafo acima, isso significa também que você pode retirar o coletor de lixo da sua aplicação Nim.

1.3 Como Nim funciona?

Uma das coisas que torna o Nim exclusivo é a sua implementação. Toda linguagem de programação tem uma implementação na forma de um aplicativo, que interpreta o código-fonte ou compila o código-fonte em um executável. Essas implementações são chamadas de intérprete e compilador, respectivamente. Alguns idiomas podem ter várias implementações, mas a única implementação da Nim é um compilador.

O compilador compila o código-fonte Nim, seu primeiro trabalho é traduzir o código para outra linguagem de programação, C, e depois passar esse código-fonte C para um compilador C, o qual o compila em um executável binário.

Esse arquivo executável contém instruções que indicam as tarefas específicas que o computador deve executar, incluindo as especificadas no código fonte Nim original. Dessa forma podemos mapear o funcionamento do Nim como um fluxo descentralizado, pois a carga de trabalho não é direcionada somente para o compilador do idioma:

Não é algo complexo de ser entendido, porém, temos muita coisa entre essas etapas, isso sim é complexo, e pode ter certeza que vamos tratar desses detalhes aqui na série, mas não nesse momento, pois nosso objetivo é somente entender como de fato funciona o processo de funcionamento da linguagem.

Bom, é legal você entender como funciona o processo de geração das ações de um aplicativo escrito em determinada linguagem de programação, no Nim funciona exatamente dessa forma, o que sempre vai te dar a garantia daqueles 3 pilares da linguagem que falamos na primeira parte da postagem.

Em uma approach mais profunda, Nim se conecta ao processo de compilação C para compilar o código-fonte C que foi gerado por ele. Isso significa que o compilador Nim depende de um compilador C externo, como GCC ou Clang. O resultado da compilação é um executável específico da arquitetura da CPU e do sistema operacional no qual foi compilado.

Isso deve dar uma boa ideia de como o código-fonte Nim é transformado em um aplicativo funcional e como esse processo é diferente do usado em outras linguagens de programação. Toda vez que você faz uma alteração em seu código fonte Nim, você precisará recompilá-lo.

1.4 Conhecendo melhor a linguagem (novamente esse tópico)

Agora que os senhores já tem uma boa base de introdução na linguagem, vou voltar nesse tópico, e falar um pouco sobre algumas das vantagens em utilizar Nim em relação a certas implementações, juntamente a desvantagens, a final de contas nem tudo são flores!

Algo que já percebi em desenvolvedores de uma única linguagem de programação, que é a constante defesa do uso, sem de fato entenderem os requisitos do use case, se você domia C ou C++, vai criar um sistema web totalmente baseado nele?

Pensar da maneira correta, em como deve ser feita a implementação durante a prototipagem de software de computador, é crucial para entender as limitações de determinada linguagem para solucinar um certo problema, acredito que com o uso de Nim, você vai encontrar diversos cenários que esse idioma pode ser uma melhor escolha que os demais, assim como o inverso vai ocorrer.

Vale lembrar que comparações de melhor linguagem de programação para determinada situação, é algo muito complexo, existem infinitos fatores de nicho para serem pensados, por esse motivo, vou sempre utilizar um padrão já conhecido, especialmente performance em aplicações especificas.

A velocidade com que as aplicações escritas em uma linguagem de programação é executada é frequentemente usada em comparações. Um dos objetivos de Nim é a eficiência, então não deve ser surpresa que seja uma linguagem de programação muito eficiente.

Normalmente a galera gosta de comparar Vala com Nim, mas não tem como chegarmos em uma comparação genérica nesse sentido, então vamos falar de performance. Como vimos logo acima, o idioma sempre vai gerar um binário intermediário, que é um código C, então é totalmente normal, dizermos que Nim, se equivale diretamente no poder de uma aplicação escrita em C.

Em comparação com C, a metaprogramação no Nim é única, pois não usa um pré-processador, mas é uma parte do processo de compilação principal. Em geral, você pode esperar encontrar muitos recursos modernos em Nim que você não encontrará em C, então escolher Nim como uma substituição C faz muito sentido.

1.4.1 Vamos tirar um racha!

Ainda sobre performance, vamos tirar um racha com o Nim e outras linguagens de programação, nosso objetivo aqui, é encontrar os números primos existentes até 1 milhão, nosso ringue vai ser composto por Nim, Python e C:

Vamos começar com o código Nim:

import math

proc eratosthenes(n:int): auto =
  result = newSeq[int8](n+1)
  result[0] = 1; result[1] = 1

  for i in 0 .. int sqrt(float n):
    if result[i] == 0:
      for j in countup(i*i, n, i):
        result[j] = 1

discard eratosthenes(100_000_000)

Agora Python (CPython):

def eratosthenes(n):
  sieve = [1] * 2 + [0] * (n - 1)
  for i in range(int(n**0.5)):
    if not sieve[i]:
      for j in range(i*i, n+1, i):
        sieve[j] = 1
  return sieve

eratosthenes(100000000)

E para finalizar, o mais vovô de todas elas, C:

#include <stdlib.h>
#include <math.h>
char* eratosthenes(int n)
{
  char* sieve = calloc(n+1,sizeof(char));
  sieve[0] = 1; sieve[1] = 1;
  int m = (int) sqrt((double) n);

  for(int i = 0; i <= m; i++) {
    if(!sieve[i]) {
      for (int j = i*i; j <= n; j += i)
        sieve[j] = 1;
    }
  }
  return sieve;
}

int main() {
  eratosthenes(100000000);
}

Vamos aos resultados:

Python:

Como de se esperar, Python demorou um tempo considerável, consumindo completamente meu processador.

C:

Como já era de se esperar, uma execução extremamente performática, mas vamos ver como Nim se sai!

Nim:

Uma performance ótima! Muito próximo da implementação em C, porém ainda sim temos um desempenho inferior nesse tipo de aplicação, lembrando que não realizamos nenhum tipo de otimização.

Nota: O resultado do compilado Nim, foi feito com a existência de um arquivo de cache.

Vale lembrar que os testes realizados aqui se basearam em um algoritmo muito simples, por tanto, podem haver inúmeras variações nos resultados exibidos aqui, e também, não considere esse teste como algo concreto! Executei apenas um ciclo, por tanto existem uma infinidade de possibilidades para variação do tempo de execução.

Testes realizados em um i3-2330M com SSD genérico.

Em um ambiente secundário que testei, vulgo meu servidor, os mesmos testes rodaram com uma deferência enorme, a mesma solução em Nim teve o resultado obtido em 2.1s, enquanto a solução em C em 2.8s, mas friso que isso pode variar muito!

1.4.2 Legibilidade

Nim é uma linguagem muito expressiva, o que significa que é fácil escrever o código, que também é claro tanto para o compilador como para o leitor humano. O código Nim não está desordenado com os colchetes e os ponto e vírgulas das linguagens de programação tipo C, como C# e C++, nem requer palavras-chave dobradas e finais que estão presentes em idiomas como o Ruby.

Vamos olhar a forma mais extensa de se fazer um loop em Nim, com a maneira mais simples e direta em C++:

for i in 0 .. <10:
  echo(i)

E agora em C++:

#include <iostream>
using namespace std;

int main()
{
   for (int i = 0; i < 10; i++)
   {
       cout << i << endl;
   }

   return 0;
}

O código Nim é mais legível e muito mais compacto. O código C++ contém muitos elementos que são opcionais no Nim, como a declaração da função principal, que é totalmente implícita no Nim.

Nim é fácil de escrever, porém mais importante ainda que isso, também é fácil de ler. A boa leitura do código é muito importante na análise humana. Por exemplo, torna a depuração mais fácil, permitindo que você gaste mais tempo escrevendo código, reduzindo seu tempo de desenvolvimento.

Isso já foi mencionado, mas vale a pena revisar para descrever como outras línguas se comparam e, em particular, por que algumas exigem um tempo de execução mais elevado que o Nim.

Linguagens de programação que são compiladas, como Nim, C, Go, C#, D, Rust, C++ e outra infinidade delas, produze um executável nativo do sistema operacional no qual o compilador está sendo executado. Compilar um aplicativo Nim no Linux resulta em um executável que só pode ser executado nesse sistema operacional, e o mesmo vale para qualquer outro sistema.

Outra coisa que entra em jogo aqui, é a arquitetura do processador do sistema hospedeiro, se você usar Nim em seu celular, que pode ser de 64 bits, em uma arquitetura ARM, significa que seu binário não vai rodar em AMD64, pois todas as instruções foram feitas para um processador de design completamente diferente.

Isso é o que esperamos de qualquer linguagem que tenha como intermédio uma outro idioma, que no caso do Nim, é o próprio C, o lado bom, é que o compilador da linguagem, pode ser instruído a compilar um executável para uma infinidade de sistemas operacionais, assim como para várias arquiteturas de processadores, deixando Nim como uma poderosa linguagem de compilação cruzada.

A compilação cruzada geralmente é usada quando um computador com a arquitetura ou sistema operacional desejado não está disponível, ou a compilação leva muito tempo. Um caso de uso comum seria compilar para dispositivos ARM, como o Raspberry Pi, onde a CPU normalmente é lenta, ou ainda sim, criar um binário executável para vários sistemas operacionais.

Vale lembrar a enorme diferença existente entre linguagens de compilação cruzada, e linguagens de plataformas cruzadas, como o Java, Perl6 e C#. As desvantagens desse modelo de computação, é o fato da dependência ser uma maquina virtual, que vai ler o compilado intermediário, e gerar assim através de um JIT a execução do mesmo, definindo assim uma plataforma de arquitetura-agnóstica, além do mais, esse modelo sempre gera inúmeros problemas de seguranças, como as famosas RCEs do .NET Framework clássico.

Python, Ruby e Perl são semelhantes. Eles também usam uma máquina virtual (VM) para executar o código. No caso de Python, uma VM é usada para otimizar a execução do código Python, mas é principalmente escondido como um detalhe de implementação do interpretador Python. O intérprete do Python analisa o código, determina quais as ações que o código está descrevendo e executa imediatamente essas ações. Não há nenhum passo de compilação como Java, C ou Nim. Mas as vantagens e desvantagens são principalmente as mesmas da JVM: Não há necessidade de compilação cruzada, mas para executar uma aplicação Python, o sistema precisa ter um intérprete Python instalado.

1.4.3 Conclusão

Fugindo um pouco do normal visto até agora, tivemos inúmeros exemplos de implementações com Nim, assim como Python e C, fique tranquilo, vamos agora para a última parte antes da introdução a prática, vamos focar nossas energias em absorver o conteúdo apresentao até aqui, faça uma colinha, é sempre bom fixar com leitura tudo aquilo que é falado aqui.

Para lembrar você, gosto de ter participação e interação, se tiver alguma dúvida, que é pertinente aos tópicos sugeridos aqui na postagem, sinta-se a vontade para falar sobre ela! Estamos aqui para isso mesmo senhores. Espero você para mais uma leitura, na parte 4!