L'intelligenza artificiale è il campo che in questo periodo progredisce in maniera più veloce.
Lavorare in questo campo è molto impegnativo in quanto l'argomento e complesso e, come se non bastasse, progredisce in maniera talmente veloce che in pochi mesi di "stasi" si rischia di diventare obsoleti. Il continuo aggiornamento e la profonda conoscenza delle diverse tecnologie, è una prerogativa imprescindibile per lavorare in questo ambito.
Siamo passati in 6 mesi da chatbot che a malapena riuscivano ad associare, spesso in maniera piuttosto generica, una frase ad un intento, a ChatGPT che compone testo con qualità simili a quello prodotto da esseri umani.
In questo percorso di continuo miglioramento, gli algoritmi diventano sempre più complessi e, per soddisfare le aspettative crescenti, diventa necessario processare moli di dati sempre più importanti.
Ecco quindi che strumenti che fino a qualche mese fa davano performance accettabili, iniziano a diventare insufficienti ed inadeguati per questa nuova era.
Ora pare che sia arrivato il momento di sostituire anche il buon e vecchio Python a fronte del più potente ed innovativo Mojo (https://www.modular.com/mojo).
Devo dire che sono veramente entusiasta di presentare Mojo sul mio blog.
Mojo è un linguaggio di programmazione che rappresenta una vera e propria "magia" per gli sviluppatori: un passo avanti rispetto ai linguaggi che lo precedono di almeno un decennio.
Combina la facilità d'uso di Python con prestazioni anche superiori a linguaggi come C++ e Rust.
È fortemente compatibile a Python: permette anche di importare pacchetti Python esistenti e utilizzarli come si è soliti fare, direttamente dagli script Mojo. Questo linguaggio offre la possibilità di sfruttare l'intero ecosistema delle librerie Python, rendendolo una miglioria interessante e a costo quasi zero, per i programmatori Python che cercano di migliorare le prestazioni senza perdere l'accesso alle risorse che già conoscono e amano.
Contemporaneamente, Mojo, offre un'infinità di potenti funzioni in aggiunta a quelle offerte da Python, alcune delle quali saranno riportate di seguito, nella parte "per nerd" di questo articolo.
Raggiunge questo obiettivo utilizzando tecnologie di compilazione di nuova generazione con caching integrato, multithreading e distribuzione in cloud. Queste caratteristiche, insieme alle funzionalità di autotuning e meta-programmazione durante la compilazione di Mojo, consentono di scrivere codice che può essere portato sugli hardware più svariati.
Uno dei principali vantaggi di Mojo è la sua capacità di diventare un "super-set" di Python nel tempo, preservando le caratteristiche dinamiche di Python e contemporaneamente aggiungendo nuove primitive per la programmazione. Queste primitive di programmazione permetteranno agli sviluppatori di Mojo di costruire librerie ad alte prestazioni che attualmente richiedono l'uso di C, C++, Rust, CUDA e altri sistemi di accelerazione.
Inoltre, Mojo è progettato per fornire un modello di programmazione unificato che funziona su vari livelli di astrazione, è accessibile ai programmatori principianti e può essere scalato per molteplici casi d'uso, dalla programmazione di acceleratori all'applicazione di scripting.
Mojo è un linguaggio di programmazione che cerca di unire il meglio dei linguaggi dinamici al meglio dei linguaggi più strutturati. Da un lato, mantiene le caratteristiche dinamiche di Python che lo rendono facile da usare e flessibile. Dall'altro, introduce novità per la programmazione che consentono di costruire librerie ad alte prestazioni, il programmatore sceglierà in ogni fase se prediligere la dinamicità e semplicità o se strutturare meglio alcuni passaggi a vantaggio delle prestazioni, avendo costantemente accesso a entrambi i mondi.
Iniziamo ad entrare più nel dettaglio e diamo un'occhiata ad alcune delle principali differenze tra Mojo e Python.
Uno dei maggiori vantaggi di Mojo è, come detto, la sua velocità. Mojo è stato progettato per essere più veloce di Python, MOLTO più veloce, il che può essere un grande vantaggio per i progetti che richiedono molta potenza di elaborazione o che devono essere eseguiti rapidamente. Inoltre, Mojo è stato progettato per essere più efficiente in termini di memoria rispetto a Python, il che significa che può gestire insiemi di dati più grandi senza esaurire la memoria.
Un aspetto collaterale positivo da non trascurare, è che in un un mondo che va sempre più verso il cloud, i container e le applicazioni serverless, approcciare Mojo in sostituzione di python significa anche fare molto saving.
Una differenza fondamentale tra Mojo e Python è la gestione della concorrenza. In Python, la concorrenza può essere un po' complicata da gestire e comunque "limitata", sia quando si ha a che fare con più thread che con più processi. Mojo è stato progettato per rendere la concorrenza più semplice ed efficiente. Questo può essere un grande vantaggio per i progetti che richiedono molte elaborazioni parallele o che devono gestire più richieste contemporaneamente.
In aggiunta a ciò, Mojo è capace autonomamente di distribuire l’elaborazione nelle GPU di eventuali schede grafiche presenti sul server.
Con l’hardware giusto, per applicazioni tipiche che si basano sull’intelligenza artificiale, secondo i benchmark, Mojo raggiunge una velocità 35000 volte superiora a quella di python! Tali prestazioni risultano non solo superiori a python, ma anche a linguaggi tipicamente considerati punto di riferimento per la velocità come ad esempio il C++.
Mojo, oltre a permettere di usare praticamente tutte le features disponibili in python, include anche una serie di funzionalità aggiuntive. Ad esempio, include il supporto integrato per l'elaborazione distribuita, che rende più facile scalare le applicazioni su più nodi. Grazie ad una serie di librerie permette di lavorare con i big data, l'apprendimento automatico e altri argomenti avanzati. Questo può essere un grande vantaggio per i progetti che richiedono questo tipo di funzionalità.
Se si lavora a un progetto che richiede molta potenza di elaborazione o che deve essere eseguito rapidamente, Mojo potrebbe essere una scelta migliore di Python. Grazie alla sua attenzione alla velocità e all'efficienza della memoria, Mojo è in grado di gestire questo tipo di compiti in modo più efficiente di Python. Se lavorate ad un progetto che richiede molta concorrenza o che deve gestire più richieste contemporaneamente, le funzioni di concorrenza di Mojo potrebbero rendervi la vita molto più facile.
Un'altra ragione per prendere in considerazione l'uso di Mojo è legata alla necessità di lavorare con i big data o con l'apprendimento automatico. Il supporto integrato di Mojo per questi argomenti può far risparmiare molto tempo e fatica rispetto al tentativo di implementare queste funzioni in Python. Inoltre, se un progetto deve essere scalato su più nodi, il supporto di Mojo per il calcolo distribuito può essere un grande vantaggio.
All'interno di una funzione, è possibile assegnare valori a un nome, e creare implicitamente una variabile della funzione, proprio come in Python. Questo è un modo molto dinamico e poco impegnativo di scrivere codice, ma rappresenta anche una sfida per due motivi:
I programmatori di sistemi spesso vogliono dichiarare un valore come costante (immutabile). Questo permette al compilatore di sostituire in fase di compilazione, il valore, rendendo il codice più veloce. Oltre alle performace, potrebbero voler ottenere un errore quando sbagliano a digitare il nome di una variabile in un'assegnazione.
Per questo Mojo supporta le dichiarazioni let e var, che introducono un nuovo valore di runtime con scope: let è costante e var è una variabile. Questi valori utilizzano lo scoping lessicale e supportano il name shadowing:
def your_function(a, b):
let c = a
# scommentare per visualizzare un errore:
# c = b # errore: c è costante
if c != b:
let d = b
print(d)
your_function(2, 3)
Le dichiarazioni let e var supportano anche la possibilità di specificare il tipo, modelli e inizializzazione tardiva:
def your_function():
let x: Int = 42
let y: F64 = 17.0
let z: F32
if x != 0:
z = 1.0
else:
z = foo()
print(z)
def foo() -> F32:
return 3.14
your_function()
La programmazione moderna richiede la capacità di costruire astrazioni che da un punto di vista devono essere di alto livello e sicure, da un'altro devono permettere controlli del layout dei dati, per un accesso ai campi in maniera sicura e strutturata. Mojo offre questa possibilità con il tipo struct.
I tipi struct sono per molti versi simili alle classi ma, mentre le classi sono estremamente dinamiche, con dispatch dinamico, monkey-patching (o "swizzling" dinamico dei metodi) e proprietà dell'istanza vincolate dinamicamente, le struct sono statiche, vincolate in fase di compilazione e vengono inserite nel loro contenitore, invece di essere implicitamente indirette e conteggiate come riferimento.
Ecco una semplice definizione di struct:
struct MyPair:
var first: Int
var second: Int
# Qui si usa 'fn' invece di 'def' - spiegheremo il perchè di seguito
fn __init__(self&, first: Int, second: Int):
self.first = first
self.second = second
fn __lt__(self, rhs: MyPair) -> Bool:
return self.first < rhs.first or
(self.first == rhs.first and
self.second < rhs.second)
La principale differenza rispetto a una classe è che tutte le proprietà di istanza di una struct devono essere dichiarate esplicitamente con "var" o "let". Questo consente al compilatore Mojo di organizzare e accedere in modo preciso ai valori delle proprietà in memoria, senza bisogno di indirizzamenti o altri costi aggiuntivi.
I campi delle strutture sono vincolati staticamente, il che significa che non vengono cercati utilizzando un dizionario di indirizzi. Di conseguenza, non è possibile cancellare o riassegnare un metodo durante l'esecuzione del programma. Questa caratteristica consente al compilatore Mojo di garantire un dispatch statico, un accesso statico ai campi e la possibilità di inserire una struct direttamente nel frame dello stack o nel tipo che la utilizza, senza bisogno di indirizzamenti o altri costi aggiuntivi.
Nonostante sia possibile utilizzare tipi dinamici come in Python, Mojo offre anche il supporto per un controllo forte dei tipi.
Una delle principali modalità per utilizzare il controllo forte dei tipi è tramite le strutture (struct) in Mojo.
Una definizione di struct in Mojo stabilisce un nome che viene legato a tempo di compilazione, e i riferimenti a tale nome in un contesto di tipo, vengono trattati come una specifica precisa per il valore definito.
Ad esempio, considera il seguente codice che utilizza la struct "MyPair" mostrata precedentemente:
def pairTest() -> Bool:
let p = MyPair(1, 2)
# Non commentare per visualizzare un errore:
# return p < 4 # da un errore durante la compilazione
return True
Se si decommenta la prima dichiarazione di ritorno e la si esegue, si otterrà un errore di compilazione che indica che 4 non può essere convertito in MyPair, che è ciò che richiede l'RHS di __lt__ (nella definizione di MyPair).
Come in Python, è possibile definire funzioni in Mojo senza specificare i tipi degli argomenti e permettere a Mojo di inferire i tipi di dati. Tuttavia, quando si desidera garantire la sicurezza dei tipi, Mojo offre anche un completo supporto per le funzioni e i metodi sovraccaricati.
Essenzialmente, ciò consente di definire più funzioni con lo stesso nome ma con argomenti diversi. Questa è una caratteristica comune presente in molti linguaggi come C++, Java e Swift.
Guardiamo un esempio:
struct Complex:
var re: F32
var im: F32
fn __init__(self&, x: F32):
"""Costruire un numero complesso dato un numero reale."""
self.re = x
self.im = 0.0
fn __init__(self&, r: F32, i: F32):
"""Costruire un numero complesso date le sue componenti reali e immaginarie."""
self.re = r
self.im = i
Puoi implementare sovraccarichi ovunque desideri: per le funzioni di modulo e per i metodi in una classe o una struct.
Mojo non supporta il sovraccarico basato esclusivamente sul tipo di risultato e non utilizza il tipo di risultato o le informazioni di tipo contestuali per l'inferenza di tipo, mantenendo le cose semplici, veloci e prevedibili. Mojo non produrrà mai un errore di "espressione troppo complessa", poiché il suo verificatore di tipo è semplice e veloce per definizione.
Le estensioni sopra descritte rappresentano il fondamento che fornisce la programmazione a basso livello e le capacità di astrazione, ma molti programmatori di sistemi preferiscono avere un controllo e una prevedibilità maggiori rispetto a quanto offerto da def in Mojo. Per riassumere, def è stato definito necessariamente come molto dinamico, flessibile e generalmente compatibile con Python: gli argomenti sono mutabili, le variabili locali sono dichiarate implicitamente al primo utilizzo e lo scoping non è applicato. Questo è ottimo per la programmazione ad alto livello e lo scripting, ma non sempre ideale per la programmazione di sistemi. Per integrare questo, Mojo fornisce una dichiarazione fn che funge da "modalità rigorosa" per def.
Dal punto di vista dell'interfaccia, fn e def sono sempre interscambiabili: non c'è nulla che un def possa fornire che un fn non possa (o viceversa). La differenza sta nel fatto che un fn è più limitato e controllato internamente al suo corpo (in modo pedante e rigoroso). In particolare, gli fn presentano alcune limitazioni rispetto ai defs:
I valori degli argomenti di default vengono considerati immutabili all'interno del corpo della funzione (come un let), anziché mutabili (come una var). Ciò permette di individuare le mutazioni accidentali e consente l'uso di tipi non copiabili come argomenti.
I valori degli argomenti richiedono una specifica di tipo (tranne per self in un metodo), in modo da evitare l'omissione accidentale delle specifiche di tipo. Allo stesso modo, l'omissione del tipo di ritorno viene interpretata come ritorno di None anziché come un tipo di ritorno sconosciuto. Si noti che entrambi possono essere dichiarati esplicitamente come ritorno di object, consentendo di optare per il comportamento di un def se desiderato.
La dichiarazione implicita delle variabili locali è disabilitata, quindi tutte le variabili locali devono essere dichiarate esplicitamente. Ciò permette di individuare gli errori di battitura dei nomi e si integra con lo scoping fornito da let e var.
Entrambi supportano il sollevamento di eccezioni, ma questo deve essere dichiarato esplicitamente su un fn con l'effetto della funzione raises, posizionato dopo l'elenco degli argomenti della funzione.
Mojo supporta la completa "semantica del valore", come nei linguaggi C++ e Swift, e semplifica la definizione di semplici aggregati di campi tramite il decoratore @value (descritto in dettaglio nel Manuale di Programmazione).
Per casi d'uso avanzati, Mojo consente di definire costruttori personalizzati (utilizzando il metodo speciale init esistente di Python), distruttori personalizzati (utilizzando il metodo speciale del esistente) e costruttori di copia e spostamento personalizzati utilizzando i nuovi metodi speciali __copyinit__ e __moveinit__.
Queste personalizzazioni a basso livello possono essere utili nella programmazione a basso livello, ad esempio con la gestione manuale della memoria. Ad esempio, considera un tipo di array in heap che deve allocare memoria per i dati durante la costruzione e deallocarla quando il valore viene distrutto:
from Pointer import Pointer
from IO import print_no_newline
struct HeapArray:
var data: Pointer[Int]
var size: Int
var cap: Int
fn __init__(self&):
self.cap = 16
self.size = 0
self.data = Pointer[Int].alloc(self.cap)
fn __init__(self&, size: Int, val: Int):
self.cap = size * 2
self.size = size
self.data = Pointer[Int].alloc(self.cap)
for i in range(self.size):
self.data.store(i, val)
fn __del__(owned self):
self.data.free()
fn dump(self):
print_no_newline("[")
for i in range(self.size):
if i > 0:
print_no_newline(", ")
print_no_newline(self.data.load(i))
print("]")
Questo tipo di array è stato implementato utilizzando funzioni di basso livello per mostrare un semplice esempio di funzionamento. Tuttavia, se lo provate, potreste rimanere sorpresi:
var a = HeapArray(3, 1)
a.dump() # Should print [1, 1, 1]
# Per visualizzare un errore, togliere il commento:
# var b = a # ERRORE: Vector non implementa __copyinit__
var b = HeapArray(4, 2)
b.dump() # Should print [2, 2, 2, 2]
a.dump() # Should print [1, 1, 1]
Il compilatore non ci consente di fare una copia del nostro array: HeapArray contiene un'istanza di Pointer (equivalente a un puntatore di livello basso in C) e Mojo non può conoscere "cosa significa il puntatore" o "come copiarlo" - questa è una delle ragioni per cui i programmatori di livello applicativo dovrebbero utilizzare tipi di livello superiore come array e slice! Più in generale, alcuni tipi (come i numeri atomici) non possono essere copiati o spostati in giro affatto, perché il loro indirizzo fornisce un'identità, proprio come avviene per un'istanza di classe.
In questo caso, vogliamo che il nostro array sia copiabile, e per abilitare ciò, implementiamo il metodo speciale __copyinit__, che convenzionalmente ha il seguente aspetto:
struct HeapArray:
var data: Pointer[Int]
var size: Int
var cap: Int
fn __init__(self&):
self.cap = 16
self.size = 0
self.data = Pointer[Int].alloc(self.cap)
fn __init__(self&, size: Int, val: Int):
self.cap = size * 2
self.size = size
self.data = Pointer[Int].alloc(self.cap)
for i in range(self.size):
self.data.store(i, val)
fn __copyinit__(self&, other: Self):
self.cap = other.cap
self.size = other.size
self.data = Pointer[Int].alloc(self.cap)
for i in range(self.size):
self.data.store(i, other.data.load(i))
fn __del__(owned self):
self.data.free()
fn dump(self):
print_no_newline("[")
for i in range(self.size):
if i > 0:
print_no_newline(", ")
print_no_newline(self.data.load(i))
print("]")
Con questa implementazione, il nostro codice precedente funziona correttamente e la copia b = a produce un'istanza logicamente distinta dell'array con una propria durata e dati.
Mojo supporta anche il metodo __moveinit__, che consente sia le mosse in stile Rust (che prende un valore quando finisce il tempo) sia le mosse in stile C++ (in cui il contenuto di un valore viene rimosso ma il distruttore viene ancora eseguito), e consente di definire logiche di spostamento personalizzate. Per ulteriori informazioni, consulta la sezione Ciclo di Vita dei Valori nel Manuale di Programmazione.
var a = HeapArray(3, 1)
a.dump() # Dovrebbe stampare [1, 1, 1]
# Questo non è più un errore:
var b = a
b.dump() # Dovrebbe stampare [1, 1, 1]
a.dump() # Dovrebbe stampare [1, 1, 1]
Mojo offre un controllo completo sulla durata di un valore, inclusa la possibilità di rendere i tipi copiabili, esclusivamente spostabili o non spostabili. Questo offre un controllo più ampio rispetto a linguaggi come Swift e Rust, che richiedono che i valori siano almeno spostabili.
Per questo articolo chiudiamo qui.
Nei prossimi giorni sarà pubblicata la seconda parte dell'articolo, completamente dedicata ai programmatori, in cui saranno chiariti altri aspetti importanti introdotti da questo nuovo linguaggio.
Per il resto, non mi rimane che incoraggiarvi a provarlo condividendo lo stupore quotidiano che provo giorno per giorno, man mano che assaporo concretamente, nei miei esperimenti, la potenza di questo nuovo strumento.