Uploaded avatar of iHiD

È il Mechanical March!

@iHiD
Oltre 3 anni fa
Video

Benvenuto al nostro secondo mese a tema: Mechanical March. Questo mese ci concentriamo sui linguaggi di sistema, quelli che vengono compilati fino al codice macchina.

Questo è in parte un post, in parte la trascrizione del video di Mechanical March. Ti darò una rapida introduzione al mese e poi daremo un'occhiata ai linguaggi di sistema, alla loro evoluzione nella storia, ai pro e ai contro della compilazione in codice macchina e, per finire, a ciascuno dei linguaggi in evidenza. Anche questa volta sono in compagnia di Erik, che nella seconda metà parlerà per la maggior parte del tempo. Ma comincio io con qualche informazione pratica.

Allora, per prima cosa i linguaggi in evidenza di questo mese. Sono C, C++, D, Go, Nim, Rust, V e Zig. Per ottenere il badge di Mechanical March devi completare cinque esercizi in uno di quei linguaggi. Il nostro track Go ha uno dei migliori sillabi su Exercism, quindi ti consiglio davvero di provarlo. Qui a Exercism siamo anche grandi fan di Nim, perché è un linguaggio relativamente semplice con cui iniziare e molto facile da scrivere, quindi ti consiglio assolutamente di provare anche quello.

Abbiamo anche cinque esercizi in evidenza da provare:

  • Linked-list o simple-linked-list (a seconda del linguaggio): allocare e liberare memoria, puntatori
  • secret-handshake: operazioni bit a bit
  • pangram: cicli for, stringhe e caratteri
  • sieve: array, cicli for
  • binary-search: array, cicli

C'è un nuovo badge, che ho annunciato nel video di aggiornamento di qualche giorno fa, per chi completa i cinque esercizi in evidenza nei linguaggi a tema. Quindi, per ottenere quel badge dovrai completare tutti quelli in un linguaggio di sistema, in un momento qualsiasi dell'anno.

Abbiamo anche un sacco di cose divertenti in cantiere: interviste con un paio di persone del team Go Core, e speriamo anche di Rust e di alcuni degli altri linguaggi. Nel corso del mese faremo anche tante dirette streaming. E presto arriveranno anche dei gadget di Mechanical March!

Allora, addentriamoci un po' di più nel lato tecnico.

A cosa servono questi linguaggi?

Be', li usiamo in tutto Exercism. La nostra CLI è scritta in Go, il nostro strumento interno di gestione dei track chiamato configlet è scritto in Nim, e la libreria principale che conta le righe di codice nelle soluzioni è scritta in Rust. Erik, perché abbiamo scelto quei linguaggi per quegli strumenti?

CLI:

  • Credo che abbiamo usato Go perché era il linguaggio con cui Katrina aveva più familiarità.
  • Go è ottimo per questi strumenti a riga di comando di dimensioni ridotte.
  • Il codice Go è relativamente semplice, il che rende più facili i contributi
  • I binari Go sono facili da rilasciare, perché non richiedono un runtime
  • Go gestisce bene la compilazione incrociata

Nim

  • Nim ha gran parte degli stessi vantaggi di Go
  • Non avevamo molte persone che conoscevano Go e che potessero mantenere la CLI, così siamo passati a Nim

In quali altri contesti ti aspetteresti di trovare questi linguaggi?

  • Ovunque le prestazioni siano importanti (driver, giochi, sistemi operativi, sistemi di build/compilatori)
  • Ovunque le risorse siano limitate (ad esempio il software embedded)
  • Tutto ciò che deve essere altamente portabile, cioè che deve girare su molte piattaforme diverse

Cos'è il codice macchina?

Come dicevo prima, i linguaggi di Mechanical March si distinguono perché vengono compilati in codice macchina. Puoi spiegare un po' cos'è il codice macchina e, per contrasto, cos'è il bytecode?

  • Il codice macchina è codice che può essere eseguito direttamente dalla macchina.
  • Il bytecode, invece, richiede altro codice che interpreti o compili il bytecode in codice macchina. Di conseguenza, il bytecode richiede un passaggio intermedio prima di poter essere eseguito.

Quali sono i pro e i contro del codice macchina rispetto al bytecode?

Pro:

  • Avvio più rapido (nessun passaggio di compilazione JIT)
  • Minore occupazione di memoria (non viene caricato alcun runtime, nessun bytecode in memoria, ottimo per i sistemi embedded)
  • Sulla macchina di destinazione non è necessario installare un runtime (importante per mantenere piccoli i container Docker)

Contro:

  • Non è portabile. Il bytecode è portabile, ma il codice macchina compilato è specifico di una piattaforma
  • Non consente ottimizzazioni avanzate come l'ottimizzazione guidata dal profilo (cioè determinare il modo migliore di compilare il codice (byte) dopo averlo eseguito per un po')

Nota: sono possibili approcci ibridi, in cui il linguaggio viene compilato in bytecode e poi si usa uno strumento diverso per compilare quel bytecode in codice macchina.

L'evoluzione della programmazione di sistema

Ok. Vediamo un po' l'evoluzione di alcuni dei linguaggi di questo mese. Per prima cosa, partiamo da dove tutto è iniziato, con il C, e da come il C++ si è evoluto da lì. Raccontaci qualcosa di questi linguaggi.

Il C è un linguaggio di livello molto basso. Sembra di essere appena un gradino sopra il codice macchina. Questo lo rende molto potente e altamente ottimizzabile, ma anche un po' incline ai bug (ad esempio eccezioni di puntatore nullo e buffer overflow). La gestione della memoria è del tutto manuale e quindi è responsabilità del programmatore, il che può portare a bug e/o perdite di memoria. Il C++ è come il C, ma con il supporto alla programmazione orientata agli oggetti. È comunque piuttosto di basso livello e richiede una gestione manuale della memoria. Sia il C che il C++ ti permettono di scrivere assembly inline (ASM)!

E per quanto riguarda i linguaggi di sistema più recenti? Come si sono evoluti?

Tutti i moderni linguaggi di programmazione di sistema supportano la gestione automatica della memoria, tramite il conteggio dei riferimenti, un garbage collector o qualche altro meccanismo.

I primi linguaggi di programmazione di sistema supportano tutti i puntatori nulli (quella che Tony Hoare ha chiamato il suo «errore da un miliardo di dollari»). Sono tristemente noti per causare errori a runtime e vulnerabilità.

Molti linguaggi moderni eliminano del tutto il null o, quanto meno, richiedono uno sforzo per usarlo.

Un altro cambiamento è il passaggio da valori mutabili per impostazione predefinita a valori immutabili per impostazione predefinita. Ad esempio, Rust e VLang hanno entrambi valori immutabili per impostazione predefinita, e per poterli modificare bisogna attivare esplicitamente la mutabilità.

Tutti i linguaggi più recenti supportano l'interoperabilità con il C (o il C++), dato che è stato scritto tantissimo codice in quei linguaggi.

Un'altra cosa interessante è che alcuni dei linguaggi più recenti non vengono compilati direttamente in codice macchina, ma usano altri strumenti per farlo. Ad esempio, Rust e Zig usano LLVM, mentre Nim permette di usare un'ampia varietà di compilatori. Questo si chiama transpiling.

E per quanto riguarda cose come le macro e la metaprogrammazione?

C'è una divisione interessante per quanto riguarda macro e metaprogrammazione. Le macro in C/C++ sono potenti, ma hanno una reputazione alquanto discutibile quanto a difficoltà d'uso. Rust, Nim e D offrono tutti una metaprogrammazione potente, ma in un modo decisamente più piacevole. Al contrario, VLang e Zig citano entrambi esplicitamente l'assenza di macro come una caratteristica del loro linguaggio, mentre Go ha un approccio diverso con go generate.

I linguaggi di sistema hanno la reputazione di essere piuttosto di basso livello. È ancora giusto dirlo?

I linguaggi più recenti lavorano tutti a livelli di astrazione più alti rispetto a C/C++. Ad esempio, Rust, D e Nim permettono anche uno stile di scrittura del codice molto funzionale. Nim e D hanno persino il concetto di funzioni «pure».

Introduzione ai linguaggi del mese

Sarebbe utile esaminare ciascuno dei linguaggi, uno per uno. Tutti i linguaggi hanno delle somiglianze: sono tutti a tipizzazione forte e statica. Ma vediamo come differiscono. Iniziamo con il C?

C

  • Sviluppato da Dennis Ritchie
  • Uno dei linguaggi più antichi e probabilmente il più usato al mondo
  • Tantissimo software è scritto in C, come Unix e Linux
  • Molto influente (pensa all'esistenza dei linguaggi «simili al C»)
  • Gestione manuale della memoria
  • Molto performante (vicino all'hardware)
  • Gira ovunque
  • Perfetto per i sistemi embedded
  • Un linguaggio piuttosto piccolo

C++

  • Sviluppato da Bjarne Stroustrup
  • Successore del C, ma con l'aggiunta dell'orientamento agli oggetti (il C con le classi)
  • Ha contribuito a rendere popolare la programmazione orientata agli oggetti
  • Più funzionalità di alto livello rispetto al C
  • Supporta la programmazione generica tramite i template
  • Aggiunge il supporto ai moduli tramite i namespace
  • Molti giochi (e motori di gioco) sono scritti in C++, così come ampie parti di Windows
  • Gestione manuale della memoria
  • Continua a evolversi, con nuove funzionalità aggiunte regolarmente (una specifica molto ampia)

D

  • Sviluppato da Walter Bright, a cui in seguito si è aggiunto Andrei Alexandrescu
  • In origine pensato come un C++ riprogettato (imparando dai suoi «errori»), prende ispirazione da molti altri linguaggi
  • Multi-paradigma, supporta la programmazione imperativa, orientata agli oggetti e funzionale
  • Facile interoperabilità con C/C++
  • Uniform Function Call Syntax
  • Compile Time Function Evaluation (ad esempio, generare una macchina a stati per le regex in fase di compilazione)
  • Supporta la programmazione funzionale e le funzioni «pure»
  • Numerose funzionalità di sicurezza
  • Sicurezza della memoria tramite @safe
  • Contratti (pre/post condizioni, invarianti)
  • Funzioni pure
  • Particolare attenzione ai test unitari, con i test accanto al codice sorgente che verificano (ha richiesto un'eccezione sul sito di Exercism :))
  • Unicode

Rust

  • Sviluppato da Graydon Hoare, dipendente di Mozilla Research, poi adottato ufficialmente da Mozilla ed oggi parte della Rust Foundation
  • Multi-paradigma, supporta la programmazione orientata agli oggetti (ma con opinioni precise, ad esempio niente ereditarietà), imperativa e funzionale (tipi Option/Result, pattern matching)
  • Molti nuovi strumenti sono scritti in Rust (ad esempio SWC, ma anche Gleam, e come secondo linguaggio supportato nel kernel Linux; dipendiamo da Rust per il contatore delle righe di codice)
  • Attenzione all'affidabilità e alle prestazioni
  • Basato su LLVM
  • Il linguaggio più amato nel sondaggio di StackOverflow negli ultimi 7 anni
  • Veloce, in parte grazie a un core e a una libreria standard minimali
  • Sicuro, sia per la memoria sia per i thread, tramite ownership e lifetimes, immutabile per impostazione predefinita
  • Sistema di tipi potente, che intercetta molti bug in fase di compilazione (soprattutto quelli legati alla memoria). Il compilatore produce messaggi di errore davvero utili
  • Tutto incluso: compilatore, strumento di build, formattatore, gestore di pacchetti, integrazioni con l'IDE
  • Documentazione splendida (esiste persino un documento enorme su come funziona il compilatore)
  • Portabile: viene compilato in un singolo binario statico e non richiede l'installazione di un runtime
  • Facile interoperabilità con il codice C
  • Astrazioni a costo zero
  • Concorrenza senza paura
  • Macro

Nim

  • Sviluppato da Andreas Rumpf (in origine chiamato Nimrod)
  • Sintassi ispirata a Python
  • Multi-paradigma
  • Usato su Exercism in configlet
  • Ottime prestazioni: iteratori a costo zero, preferenza per l'allocazione sullo stack dei tipi basati sul valore
  • Sistema di tipi moderno ed espressivo: inferenza di tipo, tuple, generici, tipi somma, async/await
  • Garbage collection, ma con il supporto a una gestione deterministica della memoria (diverse opzioni di gestione della memoria)
  • Esecuzione del codice in fase di compilazione
  • Uniform call syntax
  • Macro: facile estendere il linguaggio
  • Sistema di effetti: codificare gli effetti collaterali nel sistema di tipi

Go

  • Sviluppato da Robert Griesemer, Rob Pike e Ken Thompson presso Google
  • Per lo più imperativo/procedurale, supporta un approccio simile a quello orientato agli oggetti (ma senza ereditarietà)
  • Usato in grandi progetti come Docker e Kubernetes. Ottimo anche per backend e CLI (ad esempio esbuild)
  • Punta a essere abbastanza semplice da tenere a mente (poca sintassi)
  • Sicurezza della memoria tramite garbage collector
  • Veloce: compilazione veloce, test veloci e runtime veloce. Supporto integrato per scrivere benchmark!
  • Con opinioni precise: molto impegno per influenzare lo stile del codice Go: poca sintassi, formattazione del codice tramite go fmt, strumenti per controllare che il codice sia idiomatico, errori su cose che in altri linguaggi sono avvisi (ad esempio variabili non utilizzate), documentazione che elenca gli idiomi Go da usare
  • Sistema di tipi leggero che rende Go molto flessibile (supporta l'inferenza di tipo)
  • Portabile: viene compilato in un singolo binario statico e non richiede l'installazione di un runtime. Compilazione incrociata facile. Concorrenza tramite goroutine (leggere) e comunicazione tramite canali
  • Tipizzazione strutturale tramite le interfacce (simile al duck typing, ma verificata staticamente)
  • Gestione degli errori: il linguaggio incoraggia a controllare e gestire gli errori

VLang

  • Sviluppato da Alexander Medvednikov e Delyan Angelov
  • Ispirato a Go:
  • Stessa strategia «un solo modo di fare le cose»
  • Stessa strategia «sintassi minimale»
  • Coroutine
  • Diverso da Go:
  • Niente nil/null, ma usa un tipo risultato
  • Immutabile per impostazione predefinita
  • Tipi somma (funzionali)
  • Interpolazione di stringhe
  • Runtime e binari più piccoli
  • Pattern matching
  • Al lavoro per rendere il GC opzionale (autofree)
  • Interoperabilità con il C a costo zero
  • Genera documentazione dal codice
  • Compilatore veloce che usa poca memoria

Zig

  • Sviluppato da Andrew Kelley
  • Sintassi piuttosto ridotta (file di grammatica PEG di 500 righe)
  • Esecuzione del codice e riflessione in fase di compilazione
  • Punta a essere «ovvio», senza flussi di controllo, allocazioni o macro/metaprogrammazione nascosti
  • Allocazione manuale della memoria
  • Supporta diversi allocatori
  • Le funzioni della libreria standard che allocano hanno un parametro allocator
  • Il framework di test può rilevare perdite di memoria
  • Sicurezza:
  • Gli errori sono valori e vanno gestiti
  • Niente null, usa un tipo opzionale
  • I test si possono scrivere nello stesso file del codice sorgente (come in D)
  • Usa LLVM come backend
  • Può compilare codice C/C++
  • Facile interoperabilità con il C
  • Compilazione incrociata facile

Conclusione

Ci fermiamo qui, perché immagino che a questo punto il cervello di tutti sia bello pieno.

Spero che sia stata un'introduzione utile e divertente ai linguaggi in evidenza di questo mese. Spero che ti divertirai un sacco ad esplorare questi linguaggi. Io ed Erik saremmo entrambi felici di sapere quali sceglierai e come ti trovi, quindi scrivi pure nei commenti o sul forum!

Grazie per aver guardato!

01 marzo 2023 · Ti è stato utile?