ONTO: un linguaggio di programmazione ontologico
Saggio sul paradigma della Programmazione Orientata alle Proprietà (POP)
Introduzione
Ogni paradigma di programmazione nasce da una domanda. La programmazione procedurale chiede: come si esegue un compito? La programmazione a oggetti chiede: chi fa cosa? La programmazione funzionale chiede: cosa è una funzione? La Programmazione Orientata alle Proprietà (POP) pone una domanda più radicale: cosa significa che qualcosa esiste?
La risposta viene dall'ontologia delle proprietà sviluppata con TheoryFlow: le proprietà, e non le classi, le funzioni o i dati, sono l'unità fondamentale del discorso. Da questa ontologia discendono una logica trivalente propria, una sintassi rovesciata rispetto alle convenzioni e un modello di composizione mereologico.
Il saggio ha tre scopi: esporre i fondamenti ontologici del paradigma; descrivere il linguaggio ONTO, che li realizza insieme a un motore database; valutarne vantaggi e limiti.
Parte I — Fondamenti ontologici
1.1 L'essere e le proprietà
L'essere è la condizione fondamentale: tutto ciò che esiste è essere, e non c'è un «fuori» dell'essere. Le proprietà non sono oggetti: sono aggregati che si compongono e si scompongono mereologicamente. Non esiste prima la «palla» e poi le sue caratteristiche: esiste un aggregato coerente di proprietà che a un certo punto si riconosce come «palla».
Il libro non ha titolo, autore e anno: è il convergere di queste proprietà a produrre ciò che si chiama libro. Il passaggio dal contenitore al contenuto cambia il modo di scrivere programmi.
Ne segue il primo teorema:
Se A e B sono correlati e A è più generico di B, allora A è una proprietà di B.
La matematica è più generica dei numeri, quindi è una proprietà dei numeri, e non il contrario. Le categorie diventano obsolete: una categoria è soltanto l'aggregazione mereologica delle proprietà che la compongono. Non esiste la categoria dei colori; esistono i colori come proprietà di altri aggregati. Non c'è una proprietà più fondamentale delle altre: il sistema è distribuito e si aggrega liberamente.
1.2 Aggregazioni, non insiemi
Pensare il mondo come fatto di insiemi, cose che contengono altre cose, porta con sé problemi profondi come il paradosso di Russell. Non esistono contenitori astratti che racchiudono elementi: esistono proprietà che si aggregano.
Dire «palla rossa» non mette un oggetto dentro un insieme: indica un punto dell'essere dove certe proprietà si incontrano e si stabilizzano.
palla ▷ (la ^ rossa)
Il simbolo ▷ si legge «è aggregato da» (nel codice si può scrivere anche >>). Sostituisce le relazioni tra insiemi e sottoinsiemi: a sinistra il concetto, a destra le proprietà che lo formano.
Non tutte le aggregazioni sono costruibili. Un ghiaccio bollente o una figura contraddittoria non sono misteri logici: sono aggregazioni impossibili, perché le proprietà coinvolte non possono coesistere. Quando due proprietà sono incompatibili l'aggregazione fallisce e il sistema resta coerente. Queste aggregazioni non hanno lo statuto ontologico delle esistenze possibili nel nostro mondo; esistono solo come possibilità strutturali interne all'essere.
1.3 Perché non ci sono individui
Se un individuo è la somma delle sue proprietà e non c'è una sostanza che le porti, nulla vieta di chiamare P la proprietà che è la somma di tutte le proprietà dell'individuo. L'individuo è a sua volta un aggregato di proprietà.
Nel linguaggio ci si può riferire solo a proprietà. Dire «il tavolo» non indica un singolo tavolo: indica l'aggregato delle proprietà che costituiscono il termine. Per indicare un'esistenza specifica si aggiungono proprietà, tra cui lo spazio e il tempo, che permettono di localizzarla.
La conseguenza formale è che la logica predicativa, del primo come del secondo ordine, non è adottata: poggia sulla distinzione tra individui e predicati, che qui cade. I quantificatori non sono usati. «L'uomo è un animale» non seleziona individui: afferma che l'aggregato uomo ha tra le sue proprietà animale.
uomo ▷ animale
1.4 I tre valori
Ogni proprietà può assumere uno di tre stati epistemici.
| Valore | Significato |
|---|---|
| V (vero) | proprietà confermata: ciò che si verifica |
| F (falso) | proprietà impossibile o confutata: ciò che non si verifica |
| N (non apofantico) | proprietà indeterminata, non conoscibile direttamente: ciò che non si può verificare |
N non è il NULL dei database né un «incerto» probabilistico. Un NULL segnala un valore mancante e viene silenziato da un default; N è uno stato con tavole proprie, che entra nel calcolo.
La verità non è data a priori: si determina a posteriori, come proprietà delle teorie che sopravvivono alla confutazione.
La logica a due valori è una semplificazione di quella a tre e presuppone una struttura precisa della realtà: che negare qualcosa di falso dia il vero. È il mondo della geometria euclidea, dove esistono proprietà essenzialmente necessarie. Fuori da lì non vale: se «oggi non fa caldo» è falsa, non segue che faccia freddo. Negare una menzogna non è affermare una verità.
1.5 La logica ontologica
La negazione è asimmetrica.
!V = F
!F = N
!N = F
Nessuna negazione restituisce il vero: il vero si ottiene solo per affermazione, cioè aggiungendo informazione. La negazione fa ciclare F e N, e V resta fuori dal ciclo. Per questo !!V = N: negare due volte non riporta al punto di partenza.
La congiunzione è una compressione, la disgiunzione un'estensione.
| ^ | V | F | N | v | V | F | N | |
|---|---|---|---|---|---|---|---|---|
| V | V | F | F | V | V | V | V | |
| F | F | F | N | F | V | F | F | |
| N | F | N | F | N | V | F | N |
Il vero nasce solo da V ^ V. Un valore indeterminato non conferma una congiunzione (V ^ N = F), mentre un falso insieme a un indeterminato resta indeterminato (F ^ N = N).
Il linguaggio ha inoltre l'implicazione =>, la coimplicazione <=> e la disgiunzione esclusiva xor. Le tavole complete, la derivazione di leggi e principi a confronto con la logica booleana e le proprietà metalogiche sono in onto-rs/docs/LOGICA.md.
Tre risultati danno la misura del sistema:
- il terzo escluso e le leggi di De Morgan non valgono; il modus ponens, il modus tollens e i sillogismi sì;
- il principio di identità non vale come principio logico né ontologico:
A => Aè una tautologia,A <=> Ano. È un criterio epistemico, che descrive come gli enti si formano a partire dai determinati; - la logica classica è una porzione di quella ontologica. La negazione stretta
~(~V = F,~F = V,~N = V) si comporta come la negazione classica su V e F e serve solo a isolare quella porzione.
1.6 Il paradosso del mentitore
«Questa frase è falsa». Nel paradigma, «questa» e «falsa» sono proprietà compositive di «frase».
frase ▷ (questa ^ falsa)
La congiunzione è vera quando entrambi i legami lo sono.
frase ▷ questa: è possibile riferirsi alla frase, quindi V.frase ▷ falsa: servirebbe un metodo che verifichi a priori il valore di verità della frase. Non esiste, quindi il legame è fuori dal vero e dal falso: N.
V ^ N = F: la frase «questa frase è falsa» è falsa. Non c'è circolo, perché la negazione non riporta al vero.
Il legame di proprietà tra valori ha una sua tavola.
| ▷ | V | F | N |
|---|---|---|---|
| V | V | F | F |
| F | F | V | F |
| N | F | F | V |
È vero solo quando un valore ha come proprietà sé stesso, e non è mai non apofantico: il valore è già noto, e asserire un valore equivale ad asserire una conoscenza rispetto ad esso.
Parte II — Il paradigma POP
2.1 Il ribaltamento della classe
Nella programmazione a oggetti le proprietà di una classe sono private e prendono il nome di attributi. La dialettica si tronca a ogni proprietà, che è riservata alla classe: l'incapsulamento vincola spazi di memoria e concede i dati solo attraverso l'accesso alla classe.
Il paradigma ontologico ribalta la costruzione: una classe è unicamente un aggregato di più proprietà, e l'aggregazione di queste la vincola a non essere confusa con le altre. Le classi non sono dichiarate a priori; sono le proprietà di ciascun elemento a formarle. Si eredita soltanto l'aggregazione, e non elaborazioni costituite prima.
| OOP | POP | |
|---|---|---|
| Viene prima | la classe | l'elemento |
| La classe | si dichiara, poi si istanzia | si forma dagli elementi che ne hanno le proprietà |
| Appartenenza | dichiarata alla creazione | calcolata a ogni interrogazione |
| Nuova categoria | nuova classe, migrazione dei dati | nuovo selettore, i dati non si toccano |
| Ambito | privato, incapsulato | globale, dialettico |
2.2 La dichiarazione rovesciata
Poiché l'ontologia è compositiva e non dichiarativa, le variabili non si dichiarano come variabile = valore ma come valore = variabile. Il valore è ciò che viene associato ai nomi; pensare la variabile come un contenitore da riempire è un'abitudine del paradigma a oggetti.
42 = eta : int
"Mario" = nome : str
V = vivo : bool
Il tipo è una proprietà del valore, non una casella che lo precede. Lo stesso vale per le collezioni:
[1980, 1949, 1925] = anni : list
2.3 Elementi
Un elemento è un aggregato libero di proprietà, scritto come coppie valore=proprietà. Non ci sono schemi né costruttori.
[ "Il Nome della Rosa"=titolo, "Umberto Eco"=autore, 1980=anno ] = libro
[ "Mario Rossi"=nome, V=febbre, V=tosse, N=dispnea ] = mario
Il confronto con la stessa cosa scritta a oggetti mostra cosa cade:
class Libro:
def __init__(self, titolo, autore, anno):
self.titolo = titolo
self.autore = autore
self.anno = anno
Qui Libro è un contenitore con un costruttore: l'oggetto nasce vuoto e viene riempito. Nell'elemento le proprietà costituiscono l'aggregato; non c'è nulla da riempire.
2.4 Classi mereologiche
Una classe è definita da un selettore sulle proprietà.
[ "Mario Rossi"=nome, V=febbre, V=tosse, N=dispnea ] = mario
class { V=febbre ^ V=tosse } = Febbricitanti
L'appartenenza non si dichiara: si calcola. Un elemento inserito dopo la definizione entra nella classe se ne ha le proprietà. Le classi sono a loro volta aggregabili:
[ "Anna Bianchi"=nome, V=febbre, V=tosse, F=dispnea ] = anna
class { V=febbre ^ V=tosse } = Febbricitanti
class { Febbricitanti ^ F=dispnea } = FebbricitantiSenzaDispnea
2.5 Aggregazione
L'operatore ▷ compone un aggregato dalle proprietà presenti.
"localhost" = host : str
8080 = porta : int
aggregate config ▷ host ^ porta
config è il tutto formato da host e porta. Il tutto emerge dalle parti, non le contiene. È l'opposto dell'ereditarietà, che procede dall'alto: qui non si deriva da nulla, si compone.
2.6 Il chaining
valore : proprietà significa che il valore ha quella proprietà.
"Ciao, mondo!" : print
Se qualcosa ha la proprietà print, viene stampato: la stampa è una proprietà del valore e non un'operazione esterna che lo riceve. Il chaining ha la precedenza più bassa tra gli operatori, quindi V ^ F : print stampa il risultato della congiunzione.
2.7 Il MOVP
Il Mappatore Ontologico Vettoriale di Proprietà (MOVP) è il sistema di mappatura delle proprietà: l'espressione delle proprietà che si aggregano mereologicamente a formare un concetto unico.
C = MOVP({Pᵢ}) = ⨁ᵢ Pᵢ
Pᵢ sono le proprietà, dette semivettori; ⨁ è l'aggregazione ontologica, che non è una somma ma un'intersezione strutturale e vincolata; C è il concetto che emerge. «La mela rossa» diventa mela ▷ (la ^ rossa): la e rossa sono semivettori che aggregandosi formano il concetto, insieme alla teoria della mela che resta implicita.
In ONTO ogni elemento è un MOVP: l'aggregato delle sue proprietà trivalenti, indicizzato dal motore.
2.8 Il secondo teorema
Per qualsiasi risultato y valgono i metodi f che, applicati ai modificatori k sulle variabili x, permettono di ottenere una correlazione:
y = f(x + k).
xè il riferimento: l'ingrediente, il punto di partenza;kè il modificatore: lo strumento, l'ambiente predisposto, la variabile di controllo;fè il metodo: l'insieme di istruzioni che da x e k porta a y;yè il risultato: la teoria finale.
Ogni frase dispone di questi tre elementi più il risultato. Cambiare k cambia la teoria: sostituire l'eliocentrismo agli epicicli, a parità di orbita osservata, dà un risultato più coerente con la realtà.
def applica_metodo { x: float, k: float } -> {
return x * k
}
5.0 = massa : float
9.8 = accelerazione : float
applica_metodo{ massa=x, accelerazione=k } = forza : float
Parte III — Il linguaggio ONTO
3.1 Principi
- Il valore prima del nome: la dichiarazione rovesciata è la norma.
- Nessuna classe dichiarata a priori: esistono elementi, e classi che si formano da essi.
- Logica ontologica nativa: V, F, N sono valori primitivi con le loro tavole.
- Composizione, non ereditarietà:
▷al posto diextends. - Chaining:
:esprime l'avere una proprietà. - Linguaggio e database sono lo stesso sistema: eseguire uno script popola un motore interrogabile nello stesso script.
3.2 Sintassi in breve
Dichiarazione: valore = nome : tipo
Elemento: [ valore=proprietà, ... ] = nome
Classe: class { selettore } = Nome
Funzione: def nome { parametro: tipo, ... } -> { corpo }
Chiamata: nome{ valore=parametro, ... } = risultato : tipo
Chaining: valore : proprietà
Condizionale: if (condizione) { ... } else { ... }
Iterazione: for nome in lista_o_classe { ... }
Importazione: import "percorso.onto"
Transazione: transaction { ... }
Aggregazione: aggregate nome ▷ proprietà1 ^ proprietà2
Logica: !A ~A A ^ B A v B A xor B A => B A <=> B
Il riferimento completo è in onto-rs/docs/LINGUAGGIO.md.
3.3 Il condizionale trivalente
if (condizione) esegue il primo ramo su V e il secondo su F. Su N non esegue nessun ramo: non si agisce su una condizione indeterminata.
if (N) {
"mai eseguito" : print
} else {
"neppure questo" : print
}
Per trattare l'indeterminato servono tre controlli espliciti (== V, == F e il caso restante). Il confronto == è bivalente: A == N dice se A è non apofantico, A != N se è apofantico.
3.4 Il motore
Le proprietà trivalenti di ogni elemento entrano in un indice a bitset: per ogni proprietà un insieme dei V e un insieme degli F, con N implicito. Le congiunzioni tra proprietà sono operazioni su parole di 64 bit, e le classi si calcolano sugli stessi bitset a ogni interrogazione.
[ "Mario"=nome, V=febbre, V=tosse ] = mario
[ "Anna"=nome, V=febbre, F=tosse ] = anna
class { V=febbre ^ V=tosse } = Febbricitanti
count{ Febbricitanti=class } = n : int
search_profile{ V=febbre, V=tosse } = ids : list
top_k{ 2=k } = migliori : list
top_k ordina gli elementi per peso epistemico: la somma, sulle proprietà dell'elemento, di 2 per ogni V, 1 per ogni N e 0 per ogni F. Misura quanto di un profilo è affermato. È una misura di ordinamento del motore e non fa parte della definizione di MOVP.
Tra gli elementi si possono dichiarare relazioni tipate, attraversabili in avanti e all'indietro. Lo stato del motore si salva e si ricarica da file, oppure si tiene in una directory con un log che registra ogni scrittura e la ripristina al riavvio. Un gruppo di scritture può essere reso atomico con transaction { … }, gli elementi si modificano e si ritirano con set_property e retract, e lo stesso interprete può essere esposto in rete con onto serve.
3.5 L'interprete
L'interprete è scritto in Rust e visita direttamente l'albero sintattico, in tre fasi.
- Analisi lessicale: trasforma il testo in token. Distingue
vcome disgiunzione davcome nome, riconosce i letterali V, F, N e le stringhe con interpolazione. - Analisi sintattica: costruisce l'albero con precedenza esplicita. Dalla più bassa: chaining, coimplicazione, implicazione, disgiunzione e xor, congiunzione, confronti, aritmetica, negazioni.
- Valutazione: esegue l'albero su ambienti annidati e inserisce nel motore le proprietà trivalenti di ogni elemento creato.
Il design è descritto in onto-rs/docs/ARCHITETTURA.md.
Parte IV — Vantaggi
4.1 Coerenza tra descrizione e codice
Quando si descrive il mondo in linguaggio naturale si usano proprietà: «Mario è alto, è curioso, è ingegnere». Il paradigma porta questa struttura nel codice senza la traduzione da «descrizione di proprietà» a «definizione di classe con metodi e costruttori». La distanza tra specifica e implementazione si riduce.
4.2 Composizione senza accoppiamento
A oggetti, class C extends A, B lega C ad A e B: cambiare A può rompere C. Aggregare proprietà non crea dipendenza dall'identità della loro origine. È la soluzione al problema del gorilla e della banana: per avere una proprietà non serve importare una gerarchia.
4.3 L'indeterminato come valore
Invece di null, che porta a errori a tempo di esecuzione, o di tipi opzionali che vanno aperti a ogni uso, N è un valore di prima classe con operazioni definite. Un filtro booleano confonde «risulta falso» con «non è valutabile»; le tavole li tengono distinti.
4.4 Classi senza migrazioni
Una nuova categoria è un nuovo selettore: i dati non si toccano. Un elemento anomalo, come un mammifero che depone uova, non è un'eccezione da gestire ma una combinazione di proprietà come le altre.
4.5 Niente classi vuote
I record e le data class dei linguaggi a oggetti cercano di ridurre le classi a contenitori di dati. Qui si parte da quella posizione: un elemento è il suo aggregato di proprietà, senza costruttori né metodi di servizio.
Parte V — Limiti
5.1 Una curva di apprendimento rovesciata
Chi programma a oggetti ha interiorizzato il pensiero per classi. Pensare prima alle proprietà e poi agli aggregati non è difficile in senso tecnico, ma all'inizio disorienta, a partire dalla dichiarazione rovesciata.
5.2 Nessun polimorfismo
Il polimorfismo appartiene al paradigma a oggetti: un metodo che si comporta diversamente secondo il tipo dell'oggetto. Qui non c'è un meccanismo equivalente; se un comportamento dipende dalle proprietà di un elemento, lo si esprime con condizioni sulle proprietà.
5.3 Moduli senza spazi di nomi
import "file.onto" esegue un altro file nell'ambiente corrente, una sola volta. Non ci sono spazi di nomi separati: l'ambito è globale, coerentemente con un paradigma che considera l'incapsulamento una derivazione del pensiero per oggetti. In programmi grandi la disciplina sui nomi è a carico di chi scrive.
5.4 Interprete, non compilatore
L'albero sintattico è visitato direttamente, senza compilazione intermedia. La velocità del sistema sta nel motore, dove le interrogazioni sono operazioni su bitset; il codice ONTO che non passa dal motore è eseguito al passo di un interprete.
5.5 Logica proposizionale
La logica è proposizionale. Per le ragioni del §1.3 non ci sono quantificatori; le relazioni tra aggregati si esprimono con l'aggregazione e con le classi.
5.6 SQL come ospite
Le classi si formano sui valori di verità, sui numeri e sui testi; le relazioni si seguono da un elemento o da un'intera classe; ci sono campi unici, join per campo o con una condizione qualsiasi, aggregazioni, raggruppamenti e interrogazioni annidate. Chi arriva da un database relazionale può anche scrivere in SQL, e collegarsi con i client di PostgreSQL: le classi fanno da tabelle e le proprietà da colonne, per leggere e per scrivere. SQL resta un ospite: lo schema sono le classi, che si definiscono in ONTO, e dei tre valori di verità SQL vede il terzo come assenza di valore.
I limiti d'uso del sistema come database (join tra dati ripartiti, dati più grandi della memoria, compatibilità con PostgreSQL, ricerca per significato) sono discussi in onto-rs/docs/USE_CASES.md.
Parte VI — Confronto con altri paradigmi
6.1 POP e OOP
| OOP | POP | |
|---|---|---|
| Unità fondamentale | classe | proprietà |
| Composizione | ereditarietà, dall'alto | aggregazione, dal basso |
| Stato | incapsulato nell'oggetto | manifestato nelle proprietà |
| Logica | booleana | ontologica, a tre valori |
| Dichiarazione | tipo nome = valore |
valore = nome : tipo |
| Categorie | dichiarate | formate dagli elementi |
Il paradigma a oggetti resta preferibile dove conta l'identità di un'entità nel tempo, con stato interno che cambia: interfacce grafiche, motori di gioco, driver.
6.2 POP e programmazione funzionale
La programmazione funzionale tratta le funzioni come valori e preferisce l'immutabilità. I due paradigmi convergono sull'assenza di stato mutabile condiviso. La differenza è di orientamento: la programmazione funzionale descrive trasformazioni da un ingresso a un'uscita, POP descrive manifestazioni, un valore che ha una proprietà.
6.3 POP e i linguaggi logici
Prolog e Datalog poggiano su fatti e regole, e condividono l'idea che le relazioni siano primitive. POP non usa la risoluzione con ritorno all'indietro come meccanismo di esecuzione, non quantifica e non è bivalente.
Conclusioni
Il paradigma e il linguaggio rispondono a una domanda: cosa accade se un linguaggio è progettato come strumento di descrizione dell'essere prima che di esecuzione? Ne risulta un sistema coerente, motivato dall'ontologia e pratico nei suoi casi d'uso naturali: profili di proprietà con valori indeterminati, classificazioni che cambiano, interrogazioni per congiunzione di proprietà.
La maggior parte dei linguaggi nasce da esigenze pratiche e acquisisce una filosofia in seguito. ONTO percorre la strada opposta: parte dall'ontologia e arriva alla sintassi.
Appendice: tavole delle operazioni logiche
Tavole della logica ontologica, implementate in onto-rs/src/core/tv.rs.
Negazione
| x | !x |
|---|---|
| V | F |
| F | N |
| N | F |
Negazione stretta
| x | ~x |
|---|---|
| V | F |
| F | V |
| N | V |
Congiunzione (^)
| ^ | V | F | N |
|---|---|---|---|
| V | V | F | F |
| F | F | F | N |
| N | F | N | F |
Disgiunzione (v)
| v | V | F | N |
|---|---|---|---|
| V | V | V | V |
| F | V | F | F |
| N | V | F | N |
Implicazione (=>)
| => | V | F | N |
|---|---|---|---|
| V | V | F | F |
| F | V | V | V |
| N | V | V | V |
Coimplicazione (<=>)
| <=> | V | F | N |
|---|---|---|---|
| V | V | F | N |
| F | F | F | N |
| N | N | N | N |
Disgiunzione esclusiva (xor)
| xor | V | F | N |
|---|---|---|---|
| V | F | F | F |
| F | F | N | F |
| N | F | F | N |
Legame di proprietà tra valori (▷)
| ▷ | V | F | N |
|---|---|---|---|
| V | V | F | F |
| F | F | V | F |
| N | F | F | V |