Bem-vindo ao nosso segundo mês temático: o Mechanical March. Este mês vamos focar-nos nas linguagens de sistema, aquelas que compilam para código máquina.
Isto é em parte uma publicação, em parte uma transcrição do vídeo do Mechanical March. Vou dar-te uma introdução rápida ao mês e depois vamos olhar um pouco para as linguagens de sistema, a sua evolução ao longo da história, as vantagens e as desvantagens de compilar para código máquina, e dar uma vista de olhos rápida a cada uma das linguagens em destaque. Junto-se a mim outra vez o Erik, que será quem mais vai falar na segunda metade. Mas eu vou começar por dar-te algumas informações práticas.
Então, em primeiro lugar, as linguagens em destaque deste mês. São C, C++, D, Go, Nim, Rust, V e Zig. Para obteres o distintivo do Mechanical March, tens de completar cinco exercícios numa dessas linguagens. O nosso percurso de Go tem um dos melhores planos de estudo do Exercism, por isso recomendo mesmo que o experimentes. Também somos grandes fãs do Nim aqui no Exercism, porque é uma linguagem relativamente simples para começar e muito fácil de escrever, por isso recomendo vivamente que a experimentes também.
Temos também cinco exercícios em destaque para experimentar:
- linked-list ou simple-linked-list (consoante a linguagem): alocar/libertar memória, apontadores
- secret-handshake: operações bit a bit
- pangram: ciclos for, strings e carateres
- sieve: arrays, ciclos for
- binary-search: arrays, ciclos
Há um novo distintivo que anunciei no vídeo de atualização que fiz há alguns dias, que é por completares os cinco exercícios em destaque nas linguagens temáticas. Por isso, para obteres esse distintivo, tens de completar todos esses exercícios numa linguagem de sistema em algum momento do ano.
Também temos muitas coisas divertidas a fervilhar: entrevistas com algumas pessoas da equipa principal do Go e, esperemos, também do Rust e de algumas das outras linguagens. Vamos ter também muitas transmissões em direto ao longo do mês. E ainda vai chegar em breve merchandising do Mechanical March!
Então, vamos aprofundar um pouco mais o lado técnico.
Para que servem estas linguagens?
Bom, usamo-las por todo o Exercism. A nossa CLI está escrita em Go, a nossa ferramenta interna de gestão de percursos, chamada configlet, está escrita em Nim, e a biblioteca essencial que conta as linhas de código das tuas soluções está escrita em Rust. Erik, porque é que escolhemos essas linguagens para essas ferramentas?
CLI:
- Acho que usámos Go porque era com o que a Katrina estava mais familiarizada.
- Go é ótima para estas ferramentas de linha de comandos mais pequenas.
- O código Go é relativamente simples, o que facilita as contribuições
- Os binários de Go são fáceis de publicar, porque não precisam de um runtime
- Go faz bem compilação cruzada
Nim
- Nim tem a maior parte das mesmas vantagens que Go
- Não tínhamos muitas pessoas que soubessem Go e que pudessem manter a CLI, por isso passámos para Nim
Em que outros tipos de sítios é que esperarias encontrar estas linguagens a ser usadas?
- Em qualquer sítio onde o desempenho seja importante (controladores, jogos, sistemas operativos, sistemas de compilação/compiladores)
- Em qualquer sítio onde os recursos sejam limitados (por exemplo, software incorporado)
- Em qualquer coisa que tenha de ser altamente portável, ou seja, que corra em muitas plataformas diferentes
O que é o código máquina?
Como disse antes, estas linguagens do Mechanical March distinguem-se por compilarem para código máquina. Podes explicar um pouco o que é o código máquina e, em contraste, o que é o bytecode?
- O código máquina é código que pode correr diretamente na máquina.
- O bytecode, em contraste, precisa de outro código que interprete/compile o bytecode em código máquina. Por isso, o bytecode exige um passo intermédio antes de poder ser executado.
Quais são as vantagens e as desvantagens do código máquina em relação ao bytecode?
Vantagens:
- Arranque mais rápido (sem o passo de compilação JIT)
- Menor pegada de memória (não é preciso carregar um runtime, não há bytecode em memória, ótimo para sistemas incorporados)
- A máquina de destino não precisa de ter um runtime instalado (importante para manter os contentores Docker pequenos)
Desvantagens:
- Não é portável. O bytecode é portável, mas o código máquina compilado é específico de uma plataforma
- Não permite otimizações avançadas como a otimização guiada por perfil (determinar a melhor forma de compilar o (byte)code depois de o ter executado durante algum tempo)
Nota: são possíveis abordagens híbridas, em que a linguagem compila para bytecode e depois usa uma ferramenta diferente para compilar esse bytecode em código máquina.
A evolução da programação de sistemas
Ok. Vamos então olhar um pouco para a evolução de algumas das linguagens deste mês. Em primeiro lugar, comecemos por recuar até ao C e ver como o C++ evoluiu a partir daí. Fala-nos um pouco dessas linguagens.
C é uma linguagem de muito baixo nível. Parece que estás apenas um pouco acima do código máquina. Isso torna-a muito poderosa e altamente otimizável, mas também um pouco propensa a erros (por exemplo, exceções de apontador nulo e transbordamentos de buffer). A gestão de memória é toda manual e, por isso, da responsabilidade de quem programa, o que pode levar a erros e/ou fugas de memória. C++ é como o C, mas com suporte para programação orientada a objetos. Continua a ser bastante de baixo nível e obriga-te a fazer a gestão de memória manualmente. Tanto C como C++ permitem escrever assembly inline (ASM)!
E as linguagens de sistema mais recentes? Como é que evoluíram?
Todas as linguagens de programação de sistemas modernas suportam a gestão automática de memória, seja por contagem de referências, por um coletor de lixo ou por outro mecanismo qualquer.
As primeiras linguagens de programação de sistemas suportam todas apontadores nulos (aquilo a que Tony Hoare chamou o seu «erro de mil milhões de dólares»). Ficaram famosas por provocar erros em tempo de execução e vulnerabilidades. Muitas linguagens modernas eliminam o null ou, pelo menos, obrigam a um esforço para o usar.
Outra mudança é a passagem de valores mutáveis por omissão para valores imutáveis por omissão. Por exemplo, tanto Rust como Vlang têm valores imutáveis por omissão, sendo preciso optar explicitamente pela mutabilidade.
Todas as linguagens mais recentes suportam interoperabilidade com C (ou C++), já que há imenso código escrito nessas linguagens.
Outra coisa interessante é que algumas das linguagens mais recentes não compilam diretamente para código máquina, mas usam outras ferramentas para o fazer. Por exemplo, Rust e Zig usam o LLVM, enquanto o Nim permite usar uma grande variedade de compiladores. Isto chama-se transpilação.
E quanto a coisas como macros e metaprogramação?
Há uma divisão interessante no que toca a macros/metaprogramação. As macros em C/C++ são poderosas, mas têm uma reputação algo duvidosa de serem difíceis de utilizar. Rust, Nim e D oferecem todos metaprogramação poderosa, mas fazem-no de uma forma muito mais agradável. Em contrapartida, tanto VLang como Zig referem explicitamente a ausência de macros como uma característica da sua linguagem, e Go tem uma abordagem diferente com o go generate.
As linguagens de sistema têm a reputação de serem bastante de baixo nível. Será que isso ainda se justifica?
As linguagens mais recentes trabalham todas a níveis de abstração mais elevados do que C/C++. Por exemplo, Rust, D e Nim também permitem uma forma muito funcional de escrever código. Nim e D têm até o conceito de funções «puras».
Introdução às linguagens do mês
Então, seria bom olharmos para cada uma das linguagens, uma a uma. Todas as linguagens têm semelhanças: são todas de tipagem forte e estática. Mas vejamos como diferem. Começamos pelo C?
C
- Desenvolvida por Dennis Ritchie
- Uma das mais antigas e, provavelmente, a linguagem mais usada no mundo
- Há imenso software escrito em C, como o Unix e o Linux
- Muito influente (basta pensar nas linguagens do estilo C)
- Gestão de memória manual
- De elevado desempenho (próxima do metal)
- Corre em qualquer lado
- Perfeita para sistemas incorporados
- Uma linguagem bastante pequena
C++
- Desenvolvida por Bjarne Stroustrup
- Sucessora de C, mas com orientação a objetos acrescentada (C com classes)
- Ajudou a popularizar a programação orientada a objetos
- Mais funcionalidades de alto nível do que C
- Suporta programação genérica através de templates
- Acrescenta suporte a módulos através de namespaces
- Muitos motores de jogos são escritos em C++, tal como grandes partes do Windows
- Gestão de memória manual
- Continua a evoluir, com novas funcionalidades acrescentadas regularmente (especificação enorme)
D
- Desenvolvida por Walter Bright, e mais tarde com a participação de Andrei Alexandrescu
- Originalmente pensada como um C++ reengenhado (a aprender com os seus «erros»), inspira-se em muitas outras linguagens
- Multi-paradigma, com suporte para programação imperativa, orientada a objetos e funcional
- Interoperabilidade fácil com C/C++
- Uniform Function Call Syntax
- Compile Time Function Evaluation (por exemplo, gerar uma máquina de estados de expressões regulares em tempo de compilação)
- Suporta programação funcional e funções «puras»
- Muitas funcionalidades de segurança
- Segurança de memória através de @safe
- Contratos (pré-condições/pós-condições, invariantes)
- Funções puras
- Foco nos testes unitários, com os testes ao lado do código-fonte que testam (foi preciso abrir uma exceção no site do Exercism :))
- Unicode
Rust
- Desenvolvida por Graydon Hoare, funcionário da Mozilla Research, depois adotada oficialmente pela Mozilla e agora parte da Rust Foundation
- Multi-paradigma, com suporte para programação orientada a objetos (mas com opiniões fortes, por exemplo, sem herança), imperativa e funcional (tipos Option/Result, correspondência de padrões)
- Muitas ferramentas novas escritas em Rust (por exemplo, o SWC, mas também o Gleam, e é a segunda linguagem suportada no kernel do Linux; dependemos dela para o contador de linhas de código)
- Foco na fiabilidade e no desempenho
- Construída sobre o LLVM
- A linguagem mais adorada no inquérito do StackOverflow nos últimos 7 anos
- Rápida, em parte devido a um núcleo e a uma biblioteca padrão mínimos
- Segura, tanto em termos de segurança de memória como de segurança de threads, através de ownership e lifetimes, imutável por omissão
- Sistema de tipos poderoso, que deteta muitos erros em tempo de compilação (especialmente relacionados com memória). O compilador produz mensagens de erro muito úteis
- Tudo incluído: compilador, ferramenta de compilação, formatador, gestor de pacotes, integrações com IDE
- Documentação excelente (há até um documento enorme sobre como o compilador funciona)
- Portável: compila para um único binário estático e não requer um runtime instalado
- Interoperabilidade fácil com código C
- Abstrações de custo zero
- Concorrência sem medo
- Macros
Nim
- Desenvolvida por Andreas Rumpf (originalmente chamada Nimrod)
- Sintaxe inspirada em Python
- Multi-paradigma
- Usada no Exercism, no configlet
- Ótimo desempenho: iteradores sem overhead, preferência pela alocação na pilha de tipos baseados em valores
- Sistema de tipos moderno e expressivo: inferência de tipos, tuplos, genéricos, tipos soma, async/await
- Recolha de lixo, mas com suporte para gestão de memória determinística (várias opções de gestão de memória)
- Execução de código em tempo de compilação
- Sintaxe de chamada uniforme
- Macros: fácil de estender a linguagem
- Sistema de efeitos: codificar efeitos secundários no sistema de tipos
Go
- Desenvolvida por Robert Griesemer, Rob Pike e Ken Thompson, na Google
- Sobretudo imperativa/procedural, com suporte para uma abordagem do tipo orientado a objetos (mas sem herança)
- Usada em grandes projetos como o Docker e o Kubernetes. Também é ótima para backends e CLIs (por exemplo, o esbuild)
- Procura ser suficientemente simples para caber na cabeça (pouca sintaxe)
- Segurança de memória através do coletor de lixo
- Rápida: compilação rápida, testes rápidos e execução rápida. Suporte incorporado para escrever benchmarks!
- Com opiniões fortes: muito esforço para influenciar o estilo do código Go: pouca sintaxe, formatação do código através do go fmt, ferramentas para verificar se o código é idiomático, erro em coisas que noutras linguagens são apenas avisos (por exemplo, variáveis não utilizadas), documentação que lista os idiomatismos de Go a usar
- Sistema de tipos leve que torna Go muito flexível (suporta inferência de tipos)
- Portável: compila para um único binário estático e não requer um runtime instalado. Compilação cruzada fácil. Concorrência através de goroutines (leves) e comunicação através de canais
- Tipagem estrutural através de interfaces (semelhante ao duck typing, mas verificada estaticamente)
- Tratamento de erros: a linguagem incentiva a verificar e a lidar com os erros
VLang
- Desenvolvida por Alexander Medvednikov e Delyan Angelov
- Inspirada em Go:
- A mesma estratégia de «só uma forma de fazer as coisas»
- A mesma estratégia de «sintaxe mínima»
- Corrotinas
- Diferente de Go:
- Sem nil/null, mas usa um tipo result
- Imutável por omissão
- Tipos soma (funcionais)
- Interpolação de strings
- Runtime/binários mais pequenos
- Correspondência de padrões
- A trabalhar para tornar o GC opcional (autofree)
- Interoperabilidade com C sem custo
- Gerar documentação a partir do código
- Compilador rápido que usa pouca memória
Zig
- Desenvolvida por Andrew Kelley
- Sintaxe bastante reduzida (ficheiro de gramática PEG com 500 linhas)
- Execução de código e reflexão em tempo de compilação
- Procura ser «óbvia», sem fluxo de controlo escondido, sem alocações escondidas e sem macros/metaprogramação
- Alocação de memória manual
- Suporta diferentes alocadores
- As funções da biblioteca padrão que alocam memória têm um parâmetro de alocador
- O framework de testes consegue detetar fugas de memória
- Segurança:
- Os erros são valores e têm de ser tratados
- Sem null, usa um tipo opcional
- Os testes podem ser escritos no mesmo ficheiro que o código-fonte (como em D)
- Usa o LLVM como backend
- Consegue compilar código C/C++
- Interoperabilidade fácil com C
- Compilação cruzada fácil
Conclusão
Vamos ficar por aqui, pois imagino que, a esta altura, o cérebro de toda a gente já esteja bem cheio.
Esperemos que tenha sido uma introdução útil e divertida às linguagens em destaque deste mês. Espero que te divirtas imenso a explorar estas linguagens. Tanto eu como o Erik adoraríamos saber quais escolhes e o que achas delas, por isso deixa um comentário ou escreve no fórum!
Obrigado por veres!