← ONTO GitLab

Riferimento del linguaggio ONTO

Manuale di riferimento della sintassi implementata in onto-rs, con esempi verificati tratti da examples/. Per il design sottostante vedi ARCHITETTURA.md; per i fondamenti teorici saggio_ONTO.md e potenzialita_POP.md nella radice del repository.

Logica trivalente

Tre valori di verità, che sono stati epistemici di una proprietà:

Valore Significato
V (vero) proprietà confermata
F (falso) proprietà impossibile o confutata
N (non apofantico) proprietà indeterminata, non conoscibile direttamente

N non è un null generico: è ciò che non si può verificare, e come tale entra nel calcolo con tavole proprie.

Negazione asimmetrica, non invertibile:

!
V F
F N
N F

!!V = N ≠ V: negare due volte non torna al punto di partenza. L'incertezza, una volta introdotta, non si annulla automaticamente.

Negazione stretta ~ (anche ∼): si comporta come la negazione classica su V e F.

~
V F
F V
N V

~A vale V quando A non è affermato e coincide con A => !A. Serve solo a isolare una porzione di logica che simula quella classica: con ~, ^, v, => su valori V e F si resta in V e F con le tavole booleane: ~~A ha il valore di A, e A v ~A e ~(A ^ ~A) valgono sempre V (vedi LOGICA.md §8.5). <=> e xor restano quelli ontologici anche lì: l'equivalenza classica si scrive (A => B) ^ (B => A). Fuori da quell'uso la negazione è !.

Connettivi binari, con le tavole della logica ontologica:

^ 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
=> V F N <=> V F N xor V F N
V V F F V V F N V F F F
F V V V F F F N F F N F
N V V V N N N N N F F N
  • ^ (compressione): V solo da V ^ V; F ^ N = N; ogni altra combinazione è F. Un dato indeterminato non conferma una congiunzione: V ^ N = F.
  • v (estensione): V se almeno un operando è V; N v N = N; altrimenti F.
  • => (espansione), anche →: falsa solo con antecedente V e conseguente F o N. Dal falso e dal non apofantico segue qualsiasi conclusione.
  • <=> (coimplicazione), anche ↔: V solo da V <=> V, N se un operando è N, altrimenti F. F <=> F = F: la coimplicazione non è un'uguaglianza.
  • xor, anche ⊕: mai V; N da F xor F e N xor N; altrimenti F. Equivale a (A ^ !B) v (!A ^ B).

Precedenza, dalla più bassa: <=>, => (associativa a destra), v e xor, ^, confronti, aritmetica, ! e ~. ^ non è associativa: con valori N (a ^ b) ^ c e a ^ (b ^ c) possono differire, e una catena senza parentesi si valuta da sinistra.

Gli esempi diagnosi.onto e rischio_credito.onto mostrano l'uso pratico: un sintomo indeterminato rende «non confermata» la diagnosi (V ^ N = F), e un requisito mancato insieme a uno indeterminato lascia la domanda in verifica (F ^ N = N).

Tavole, principio di identità e derivazione computazionale di tutte le leggi logiche sono in LOGICA.md e LEGGI_LOGICHE.md.

Confronti: ==, !=, <, >, <=, >= su numeri e stringhe; il risultato è V o F. Tra valori trivalenti == confronta i valori e restituisce V anche per N == N: va letto come confronto epistemico tra valori (stesso valore), e non come principio d'identità (vedi LOGICA.md, §4).

Dichiarazione rovesciata

valore = nome [: tipo] — il valore precede il nome: si dichiara prima cosa è la cosa, poi come si chiama.

onto
"Ciao, mondo!" = messaggio : str
42 = risposta : int
V = vero : bool

Il tipo, quando presente, valida il valore secondo questa tabella:

Tipo Accetta
int interi
float float e interi
str stringhe
bool valori trivalenti V/F/N
list liste
element elementi/aggregati
null il valore nullo
any (o qualsiasi altro nome non riconosciuto) qualunque valore, nessun controllo

Elementi

Un elemento è un aggregato libero di proprietà: niente schemi né classi dichiarate a priori, solo coppie valore=proprieta in forma libera.

onto
[ "Mario Rossi"=nome, V=febbre, V=tosse ] = mario
mario.febbre : print   # V

I campi con valore trivalente (V/F/N) vengono inseriti automaticamente come record nel motore trivalente alla creazione dell'elemento; i campi di altro tipo (stringhe, numeri, liste) restano nel solo aggregato e sono accessibili con .campo. Ogni elemento così creato ha un id nel motore, usato dalle funzioni built-in e dalle relazioni. Vedi ARCHITETTURA.md e examples/motore_db.onto.

Classi mereologiche

Una classe è un aggregato intensionale di elementi, definita da un selettore sui valori trivalenti delle proprietà. La membership non si dichiara: si calcola sul motore a ogni interrogazione.

onto
class { V=febbre ^ V=tosse } = Febbricitanti

Selettori

Precedenza: v bassa, ^ alta, parentesi per raggruppare.

  • espr = proprieta — condizione su una proprietà: l'espressione viene valutata a trivalente e confrontata (V=febbre, F=dispnea, N=esame); se vale un numero o un testo è un'uguaglianza su quel campo ("Eco"=autore, 1980=anno);
  • proprieta <confronto> valore — condizione su un numero o un testo, con <, <=, >, >=, ==, != (anno >= 1950). Vale V dove la relazione è soddisfatta, F dove l'elemento ha il campo ma non la soddisfa, N dove l'elemento non ha quel campo;
  • NomeClasse — riferimento a un'altra classe: le classi si aggregano in classe-in-classe (class { Febbricitanti ^ F=dispnea });
  • ^ / v / ( ... ) — congiunzione, disgiunzione, raggruppamento.

Classi dinamiche (lazy)

Un elemento inserito dopo la definizione della classe ne diventa membro automaticamente se soddisfa il selettore: il bitset dei membri V si ricalcola a ogni interrogazione. Esempio completo in examples/classi.onto.

Classi fisse

class fixed { selettore } = Nome congela i membri nell'istante della definizione: i nuovi elementi che soddisferebbero il selettore non vi entrano. Utile per snapshot e rilevazioni storiche.

Membership

onto
[ "Mario Rossi"=nome, V=febbre, V=tosse, F=dispnea ] = mario
class { V=febbre ^ V=tosse } = Febbricitanti

if (mario in Febbricitanti) { "febbricitante" : print }   # V o F

L'operatore in valuta l'appartenenza corrente dell'elemento alla classe.

Aggregazione di classi

Un selettore può referenziare altre classi, che si compongono con ^ e v:

onto
[ "Mario Rossi"=nome, V=febbre, V=tosse, F=dispnea ] = mario
class { V=febbre ^ V=tosse } = Febbricitanti
class { Febbricitanti ^ F=dispnea } = FebbricitantiSenzaDispnea

Le definizioni cicliche (una classe che referenzia se stessa, anche indirettamente) sono un errore a runtime.

Iterazione sui membri

onto
[ "Mario Rossi"=nome, V=febbre, V=tosse, F=dispnea ] = mario
class { V=febbre ^ V=tosse } = Febbricitanti

for id in Febbricitanti {
    "membro id={id}" : print
}

Itera sugli id correnti dei membri nel motore. for accetta anche liste (for x in [1, 2, 3] o for x in lista); vedi "Iterazione" sotto.

Built-in count e members

onto
[ "Mario Rossi"=nome, V=febbre, V=tosse, F=dispnea ] = mario
class { V=febbre ^ V=tosse } = Febbricitanti

count{ Febbricitanti=class } = n : int    # numero di membri correnti
members{ Febbricitanti=class } = ids : list # id dei membri correnti

Funzioni

onto
def quadrato { n: int } -> {
    return n * n
}

quadrato{ 5=n } = ris : int   # 25

nome{valore=campo, ...} è sempre una chiamata: di una funzione utente definita con def, oppure di una funzione built-in del motore (vedi sotto). Una funzione utente prevale su un built-in con lo stesso nome. Una chiamata usata come statement senza = nome invoca e scarta il risultato (utile per gli effetti collaterali, come print). Le funzioni non hanno sintassi con parentesi: quadrato(5) non è valido.

return dentro un ciclo for propaga fino alla funzione chiamante, non si ferma al ciclo.

Chaining

valore : proprietà : proprietà : ... applica proprietà in sequenza, con la precedenza più bassa di tutti gli operatori: si applica all'espressione intera a sinistra.

onto
"ONTO" = progetto : str
[ "Mario Rossi"=nome, V=febbre ] = mario

"Il progetto è: {progetto}" : print
V ^ F : print                 # F
mario.febbre : print          # V (accesso a campo via chaining)

print è una proprietà built-in; se il nome dopo i due punti corrisponde a una funzione utente con almeno un parametro, il valore corrente viene passato come primo argomento (valore : trasforma : print).

Condizionale

onto
V = condizione : bool

if (condizione) {
    "ramo eseguito su V" : print
} else {
    "ramo eseguito su F" : print
}

La condizione è convertita in trivalente; il ramo if esegue solo su V, il ramo else solo su F. Se la condizione è N, nessun ramo esegue: non è un errore, è la semantica dell'indeterminato — "non ci sono abbastanza informazioni per decidere". Il ramo else è opzionale.

Iterazione

onto
[1980, 1949, 1925] = lista : list

for elemento in lista {
    elemento : print
}

L'iterabile deve essere un identificatore o un letterale [a, b, c] (non un'espressione arbitraria: la grammatica evita l'ambiguità tra la { del corpo del ciclo e la { di una chiamata lista{...}). Accetta anche una classe (itera sugli id dei membri correnti).

Aggregazione mereologica ▷

onto
"localhost"=host : str
8080=porta : int
V = attivo : bool

aggregate config ▷ host ^ porta ^ attivo

▷ si legge «è aggregato da». Dove il carattere non è comodo da digitare si può scrivere >>, che è equivalente: aggregate config >> host ^ porta.

Compone l'aggregato config dalle proprietà nominate già presenti nell'ambiente (bottom-up, senza costruttori né ereditarietà): ogni nome dopo ▷ diventa un campo, con il valore che quella variabile ha in ambiente. Più proprietà possono essere elencate in un'unica istruzione con ^, oppure una per istruzione (aggregate config ▷ host poi aggregate config ▷ porta): le istruzioni successive sovrascrivono o aggiungono campi a un aggregato esistente con lo stesso nome. Se config non esiste ancora, viene creato. L'aggregato risultante è accessibile con config.campo e, se contiene valori trivalenti, è presente anche nel motore.

Ricerca su numeri e testi

I campi numerici e testuali di un elemento sono indicizzati in ordine, e si cercano per valore esatto o per intervallo:

onto
[ "Il Nome della Rosa"=titolo, "Eco"=autore, 1980=anno ] = libro1
[ "1984"=titolo, "Orwell"=autore, 1949=anno ] = libro2
[ "Il Processo"=titolo, "Kafka"=autore, 1925=anno ] = libro3

search_val{ "autore"=property, "Orwell"=value } = di_orwell : list
search_range{ "anno"=property, 1900=min, 1950=max } = primo_novecento : list
search_range{ "anno"=property, 1950=min } = dal_1950 : list

Gli estremi sono inclusi e uno dei due può mancare. Sui testi l'ordine è quello alfabetico per codice dei caratteri. Interi e decimali si confrontano tra loro per valore.

Le stesse condizioni entrano nei selettori delle classi, insieme a quelle sui valori di verità:

onto
[ "Il Nome della Rosa"=titolo, "Eco"=autore, 1980=anno, V=classico ] = libro1
[ "1984"=titolo, "Orwell"=autore, 1949=anno, V=classico ] = libro2

class { V=classico ^ anno < 1950 } = ClassiciAntichi
class { "Eco"=autore v anno > 2000 } = EcoODuemila

Ricerca a testo libero

onto
[ "Il Nome della Rosa"=titolo ] = libro1
[ "La rosa bianca"=titolo ] = libro2
[ "Le rose del deserto"=titolo ] = libro3
[ "Rosso Malpelo"=titolo ] = libro4

search_text{ "titolo"=property, "rosa"=query } = con_rosa : list
search_text{ "titolo"=property, "nome ROSA"=query } = entrambe : list
search_text{ "titolo"=property, "rosa deserto"=query, "any"=match } = almeno_una : list
search_text{ "titolo"=property, "ros*"=query } = per_prefisso : list

Trova gli elementi il cui testo contiene tutte le parole richieste, in qualunque ordine; con "any"=match ne basta una. Il risultato è in ordine di rilevanza: pesa di più una parola rara tra gli elementi, una parola ripetuta nel testo e un testo breve.

Le parole si confrontano per radice, con l'algoritmo Snowball per l'italiano: senza distinguere maiuscole, accenti e forme flesse. rosa trova «rose», citta trova «città», guardare trova «guardando» e «guardandogli». Una parola che termina con * vale come prefisso. Più parole tra virgolette sono una frase, che il testo deve contenere di seguito e in quell'ordine: "\"rosa bianca\" giardino". Una parola seguita da ~ trova anche quelle che ne differiscono per una lettera (due, se è lunga): automobole~ trova «automobile».

Per testi in un'altra lingua, text_language{ "english"=language } sceglie l'algoritmo di quella lingua e reindicizza i testi già presenti. Le lingue sono: italiano (predefinita), inglese, francese, tedesco, spagnolo, portoghese, olandese, svedese, norvegese, danese, finlandese, rumeno, ungherese, russo, greco, turco, arabo, tamil; "nessuna" lascia le parole come sono. Con "titolo"=property la lingua vale per i testi di quella sola proprietà, così nello stesso archivio convivono testi in lingue diverse; i sinonimi dichiarati valgono in ognuna.

I sinonimi si dichiarano, uno alla volta o da un file con un gruppo per riga (parole separate da virgole), e formano gruppi. synonym{ "th_it_IT_v2.dat"=thesaurus } carica invece un intero dizionario dei sinonimi nel formato dei tesauri di LibreOffice (per l'italiano circa 26 000 voci, in mezzo secondo): lì ogni parola porta con sé i propri sinonimi, senza che i sinonimi di un sinonimo si aggiungano.

onto
[ "Un'automobile rossa"=testo ] = annuncio

synonym{ "auto"=word, "automobile"=same }
search_text{ "testo"=property, "auto"=query } = trovati : list

Ricerca per vicinanza

Una lista di numeri in un elemento è un vettore: la sua posizione in uno spazio in cui conta la vicinanza. nearest trova gli elementi più vicini a un vettore dato, dal più vicino (per coseno dell'angolo).

onto
[ "gatto"=nome, [0.9, 0.1, 0.0]=posizione ] = gatto
[ "tigre"=nome, [0.8, 0.3, 0.1]=posizione ] = tigre
[ "tavolo"=nome, [0.0, 0.1, 0.9]=posizione ] = tavolo

nearest{ "posizione"=property, [1, 0, 0]=vector, 2=k } = vicini : list

Serve quando la posizione la calcola qualcun altro, per esempio un modello che trasforma testi o immagini in vettori: ONTO li conserva e trova i più vicini. Con of si restringe a una classe.

Di norma nearest confronta il vettore dato con quello di ogni elemento, e il risultato è esatto. Quando i vettori sono molti, vector_index dà alla proprietà un indice dei vicini: un grafo che collega ogni vettore ai suoi prossimi, su cui la ricerca ne visita una piccola parte.

onto
vector_index{ "posizione"=property }
nearest{ "posizione"=property, [1, 0, 0]=vector, 2=k } = vicini : list
nearest{ "posizione"=property, [1, 0, 0]=vector, 2=k, V=exact } = esatti : list

L'indice si costruisce alla dichiarazione, segue ogni modifica ed è salvato con lo stato. Ha un costo a ogni vettore inserito, e il risultato è approssimato: di norma contiene quasi tutti i vicini veri, non è garantito che li contenga tutti. V=exact chiede il confronto con tutti. Sotto i mille vettori l'indice non viene usato, perché il confronto con tutti è già rapido.

Una misura, su 100 000 vettori casuali a 64 dimensioni, che per un indice di questo genere sono il caso più sfavorevole: costruirlo ha richiesto 145 secondi; una ricerca dei dieci più vicini 5 millisecondi invece di 114, e ha trovato in media 9,2 dei dieci vicini veri. Con vettori che hanno una struttura, come quelli calcolati dai modelli, ne trova di più.

Ricerca per significato

similar_text trova i testi più vicini per significato a una domanda, anche quando non ne contengono le parole. Il significato è ricavato dai testi dell'archivio, senza modelli esterni: due parole sono vicine se compaiono negli stessi contesti, e un testo è la somma pesata delle sue parole (le rare contano più delle comuni).

onto
[ "il medico visita il paziente in ambulatorio"=nota ] = n1
[ "il dottore visita il paziente in ospedale"=nota ] = n2
[ "il medico prescrive la terapia al malato"=nota ] = n3
[ "il dottore prescrive la terapia al malato"=nota ] = n4
[ "il cuoco prepara il sugo in cucina"=nota ] = n5
[ "il cuoco taglia le verdure per la zuppa"=nota ] = n6

similar_text{ "nota"=property, "dottore"=query, 4=k } = vicine : list

Qui «medico» e «dottore» compaiono negli stessi contesti: la domanda «dottore» trova prima le note che contengono la parola, poi quelle con «medico», e solo dopo quelle di cucina. Con of si cerca in una classe.

Ciò che similar_text sa delle parole lo impara dai testi che ha: con poche decine di testi distingue gli argomenti, con migliaia riconosce parole di significato vicino; non conosce ciò che nei testi non compare. Il modello si costruisce alla prima ricerca e occupa circa 1,4 KB per testo; i testi aggiunti dopo vi entrano subito, e quando superano un ventesimo del totale il modello si ricostruisce. Il confronto è con ogni testo. Su 120 000 testi di sessanta parole la costruzione ha richiesto 16 secondi e ogni ricerca circa 0,3. Per un significato imparato da grandi quantità di testi esterni si usano i vettori di un modello dedicato, con nearest.

Vincoli

onto
unique{ "email"=property }

[ "Mario"=nome, "mario@esempio.it"=email ] = mario

unique dichiara unico un campo numerico o testuale: creare un secondo elemento con lo stesso valore è un errore, e dentro una transaction annulla l'intero blocco. Dichiarare unico un campo che ha già doppioni è a sua volta un errore. Le relazioni hanno un vincolo implicito: add_relation rifiuta un estremo che non esiste o è stato ritirato.

Join

onto
[ "Mario"=nome, "Bergamo"=citta, V=attivo ] = mario
[ "Anna"=nome, "Milano"=citta, V=attivo ] = anna
[ "Sede Nord"=nome, "Bergamo"=luogo ] = sede

class { V=attivo } = Attivi
join{ Attivi=left, [sede]=right, "citta"=left_on, "luogo"=right_on } = coppie : list

join accoppia gli elementi di due lati che hanno lo stesso valore di un campo numerico o testuale e restituisce le coppie [id_sinistro, id_destro]. I lati sono classi, liste o elementi. Se il campo ha lo stesso nome sui due lati basta "campo"=on.

Per una condizione qualsiasi si passa una funzione di due elementi con where: restano le coppie per cui vale V. Da sola prova tutte le coppie; insieme a un campo, filtra quelle che il campo ha già accoppiato.

onto
[ "Mario"=nome, 30=eta ] = mario
[ "Anna"=nome, 45=eta ] = anna
[ "Corso base"=titolo, 25=eta_minima ] = base
[ "Corso senior"=titolo, 40=eta_minima ] = senior

def idoneo { persona: element, corso: element } -> {
    return persona.eta >= corso.eta_minima
}

join{ [mario, anna]=left, [base, senior]=right, idoneo=where } = idonei : list

Aggregazioni

onto
[ "Mario"=nome, "Bergamo"=citta, 30=eta, V=attivo ] = mario
[ "Anna"=nome, "Milano"=citta, 45=eta, V=attivo ] = anna
[ "Luca"=nome, "Bergamo"=citta, 52=eta, F=attivo ] = luca

class { eta >= 0 } = Persone
class { V=attivo } = Attivi

sum{ Persone=of, "eta"=property } = totale : int
avg{ Attivi=of, "eta"=property } = media_attivi : float
max{ Persone=of, "eta"=property } = massima : int
count{ Persone=of } = quanti : int
group{ Persone=of, "citta"=property } = per_citta : list

sum, avg, min, max operano su un campo degli elementi di una classe, di una lista o di un singolo elemento; chi non ha il campo è saltato. Senza valori la somma è 0 e gli altri risultati sono N. group raggruppa per valore di un campo e restituisce [valore, [id, ...]]. Poiché ogni argomento of, left, right, from, to accetta il risultato di un'altra interrogazione, le interrogazioni si compongono l'una dentro l'altra: avg{ members{ Attivi=class }=of, "eta"=property }. element{ 3=id } restituisce l'elemento con quell'id.

SQL

sql{ "..."=query } esegue un'istruzione SQL. Le tabelle sono le classi (più elementi, tutti gli elementi), le colonne sono le proprietà, id è l'identificativo dell'elemento. Una SELECT restituisce le righe come lista di liste.

onto
[ "Mario"=nome, "Bergamo"=citta, 30=eta, V=attivo ] = mario
[ "Anna"=nome, "Milano"=citta, 45=eta, V=attivo ] = anna
[ "Luca"=nome, "Bergamo"=citta, 52=eta, F=attivo ] = luca
[ "Sede Nord"=sede, "Bergamo"=luogo ] = nord

class { eta >= 0 } = Persone
class { "Bergamo"=luogo } = Sedi

sql{ "SELECT nome, eta FROM Persone WHERE eta > 40 AND attivo = V ORDER BY eta DESC"=query } = r : list
sql{ "SELECT citta, COUNT(*) AS n, AVG(eta) FROM Persone GROUP BY citta HAVING COUNT(*) > 1"=query } = per_citta : list
sql{ "SELECT p.nome, s.sede FROM Persone p LEFT JOIN Sedi s ON p.citta = s.luogo ORDER BY p.nome"=query } = coppie : list
sql{ "SELECT nome FROM Persone WHERE citta IN (SELECT luogo FROM Sedi) AND nome LIKE '%o'"=query } = in_sede : list
sql{ "SELECT citta FROM Persone UNION SELECT luogo FROM Sedi ORDER BY 1"=query } = luoghi : list
sql{ "SELECT UPPER(nome) || '!', eta * 12 AS mesi FROM Persone WHERE (eta + 5) % 2 = 1"=query } = calcoli : list
sql{ "SELECT citta, MAX(eta) - MIN(eta), ROUND(SUM(eta) / COUNT(*), 1) FROM Persone GROUP BY citta"=query } = scarti : list
sql{ "SELECT MAX(n) FROM (SELECT citta, COUNT(*) AS n FROM Persone GROUP BY citta) per_citta"=query } = massimo : list
sql{ "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'Persone'"=query } = colonne : list
sql{ "WITH adulti AS (SELECT nome, citta, eta FROM Persone WHERE eta >= 18) SELECT citta, COUNT(*) FROM adulti GROUP BY citta"=query } = per_citta_adulti : list
sql{ "SELECT nome, CASE WHEN eta > 50 THEN 'senior' ELSE 'junior' END FROM Persone ORDER BY nome"=query } = fasce : list
sql{ "SELECT nome, RANK() OVER (PARTITION BY citta ORDER BY eta DESC), SUM(eta) OVER (ORDER BY eta) FROM Persone"=query } = posti : list
sql{ "SELECT nome, (SELECT sede FROM Sedi s WHERE s.luogo = p.citta) FROM Persone p WHERE EXISTS (SELECT 1 FROM Sedi s WHERE s.luogo = p.citta)"=query } = con_sede : list

sql{ "INSERT INTO Persone (nome, citta, eta, attivo) VALUES ('Sara', 'Roma', 61, N)"=query } = nuovi : list
sql{ "UPDATE Persone SET citta = 'Torino', attivo = F WHERE eta > 60"=query }
sql{ "DELETE FROM Persone WHERE nome = 'Luca'"=query }

sql{ "WITH RECURSIVE serie(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM serie WHERE n < 5) SELECT SUM(n) FROM serie"=query } = quindici : list
sql{ "SELECT nome, AVG(eta) OVER (ORDER BY eta ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM Persone"=query } = medie_mobili : list
sql{ "SELECT nome FROM Persone WHERE eta = ANY (ARRAY[30, 45])"=query } = scelti : list

sql{ "CREATE TABLE libri (isbn text PRIMARY KEY, titolo text, anno int)"=query }
sql{ "INSERT INTO libri (isbn, titolo, anno) VALUES ('88-06', 'Il nome della rosa', 1980)"=query }
sql{ "SELECT titolo FROM libri WHERE anno < 2000"=query } = vecchi : list
sql{ "CREATE TABLE prestiti (numero int PRIMARY KEY, isbn text NOT NULL REFERENCES libri (isbn), giorni int DEFAULT 30)"=query }
sql{ "INSERT INTO prestiti (numero, isbn) VALUES (1, '88-06')"=query }
sql{ "CREATE VIEW in_prestito AS SELECT l.titolo, p.giorni FROM prestiti p JOIN libri l ON l.isbn = p.isbn"=query }
sql{ "SELECT titolo FROM in_prestito WHERE giorni >= 30"=query } = fuori : list

Le forme accettate:

text
[WITH [RECURSIVE] nome [(colonna, ...)] AS (SELECT ...), ...]
SELECT [DISTINCT] voce [AS nome], ...
[FROM tabella [alias] { [LEFT] JOIN tabella [alias] ON condizione | , tabella [alias] }]
     (una tabella può essere un'interrogazione: FROM (SELECT ...) nome)
[WHERE condizione] [GROUP BY colonna, ...] [HAVING condizione]
{ UNION [ALL] SELECT ... }
[ORDER BY termine [ASC|DESC], ...] [LIMIT n] [OFFSET n]

INSERT INTO tabella (colonna, ...) VALUES (valore, ...), ...
UPDATE tabella SET colonna = valore, ... [WHERE condizione]
DELETE FROM tabella [WHERE condizione]

CREATE TABLE [IF NOT EXISTS] tabella (
    colonna tipo [PRIMARY KEY | UNIQUE] [NOT NULL] [DEFAULT valore]
                 [REFERENCES tabella [(colonna)]], ...)
ALTER TABLE tabella ADD [COLUMN] colonna tipo [vincoli]
DROP TABLE [IF EXISTS] tabella
CREATE [OR REPLACE] VIEW vista AS SELECT ...
DROP VIEW [IF EXISTS] vista
  • Una voce è * (gli id), oppure un'espressione fatta di colonne, valori, operazioni (+, -, *, /, %, || per unire testi), funzioni (UPPER, LOWER, TRIM, LENGTH, SUBSTR, CONCAT, REPLACE, ABS, ROUND, COALESCE, NULLIF, GREATEST, LEAST), conversioni (CAST(x AS int), x::text), CASE WHEN ... THEN ... ELSE ... END, un'interrogazione tra parentesi che dà un valore, e aggregati (COUNT(*), COUNT, SUM, AVG, MIN, MAX di un'espressione). La divisione tra interi resta intera solo se è esatta; un'operazione con un valore che manca non dà valore.
  • Le condizioni confrontano colonne, valori e (in HAVING) aggregati con =, !=, <>, <, <=, >, >=, e ammettono BETWEEN, LIKE (% e _), ~ e ~* (espressioni regolari), IN (valori), IN (SELECT ...), EXISTS (SELECT ...), IS [NOT] NULL, AND, OR, NOT e parentesi. Un'interrogazione dentro un'altra può riferirsi alla riga esterna scrivendo la colonna con il nome della sua tabella (p.citta).
  • I valori sono numeri, stringhe tra apici singoli e i tre valori di verità V, F, N (anche TRUE, FALSE, NULL): attivo = N trova chi ha quella proprietà indeterminata. Un confronto con una colonna che l'elemento non ha non è vero; IS NULL trova proprio chi non ce l'ha.
  • ORDER BY accetta colonne, aggregati, nomi dati con AS e posizioni (ORDER BY 2 DESC).
  • INSERT crea elementi e ne restituisce gli id. Un elemento appartiene alle classi di cui ha le proprietà, quindi il nome della tabella conta solo per quelle create con CREATE TABLE. UPDATE imposta valori di verità, numeri e testi; DELETE ritira gli elementi.
  • Una lista si scrive ARRAY[1, 2, 3] o si ricava da un'interrogazione con ARRAY(SELECT ...). lista[2] ne prende un elemento (si conta da 1), ARRAY_LENGTH(lista, 1) li conta, ARRAY_TO_STRING(lista, ', ') li unisce, STRING_TO_ARRAY fa il contrario; x = ANY (lista) è vero se il confronto vale per almeno un elemento, x > ALL (lista) se vale per tutti (la lista può essere un'interrogazione). Una lista di soli numeri inserita in un elemento è un vettore, che nearest sa cercare.

WITH dà un nome a un'interrogazione, da usare poi come una tabella. Le funzioni a finestra calcolano un valore per ogni riga guardando le altre: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, FIRST_VALUE, LAST_VALUE, e gli aggregati seguiti da OVER. PARTITION BY divide le righe in gruppi, ORDER BY le ordina dentro ogni gruppo; un aggregato con ORDER BY vale dall'inizio del gruppo fino alla riga corrente, oppure sulle righe indicate da una cornice: ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING prende la riga prima, quella corrente e quella dopo. Gli estremi sono UNBOUNDED PRECEDING, n PRECEDING, CURRENT ROW, n FOLLOWING, UNBOUNDED FOLLOWING. Negli esempi sopra: le persone per città tra gli adulti, la fascia d'età di ognuno, il posto per età nella propria città con la somma progressiva delle età, e la sede della città di chi ne ha una.

WITH RECURSIVE dichiara una tabella che usa sé stessa: una parte iniziale e, dopo UNION, una parte che ricava righe nuove da quelle trovate al passo prima, finché ne trova. Serve per le serie, le gerarchie (chi dipende da chi, a ogni livello) e le visite di un grafo. Con UNION senza ALL una riga già trovata non conta come nuova, ed è così che la visita di un grafo con cicli si ferma; oltre un milione di righe l'interrogazione è interrotta.

CREATE TABLE definisce una tabella in SQL: è una classe i cui membri sono gli elementi inseriti con quel nome. Le sue colonne sono quelle dichiarate, e ciò che si dichiara è imposto a ogni INSERT e UPDATE:

  • il tipo: numeri interi (int, bigint, serial), numeri (numeric, real, double precision), testi (text, varchar, date, uuid, …), valori di verità (boolean), liste (tipo[]); un tipo che ONTO non distingue accetta qualunque valore, e una data resta un testo;
  • NOT NULL: la colonna non può mancare né valere N;
  • DEFAULT valore: il valore di chi non nomina la colonna;
  • PRIMARY KEY e UNIQUE: due righe della tabella non hanno lo stesso valore (tabelle diverse possono avere colonne con lo stesso nome);
  • REFERENCES tabella (colonna): il valore deve esistere in quella colonna dell'altra tabella, e una riga a cui altre si riferiscono non si può togliere né cambiare in quella colonna.

Una colonna non dichiarata è rifiutata; ALTER TABLE ... ADD COLUMN ne aggiunge una, e con DEFAULT la dà anche alle righe che ci sono già. DROP TABLE ritira gli elementi della tabella e ne toglie la classe. CREATE INDEX è accettato senza effetto, perché gli indici ci sono già. Le classi definite in ONTO restano com'erano: nessuna colonna dichiarata, nessun tipo imposto.

CREATE VIEW dà un nome a un'interrogazione: da lì in poi la vista si interroga come una tabella, anche insieme ad altre, e ogni volta legge i dati del momento. Una vista non si scrive: si scrive nelle tabelle da cui legge.

Lo schema si legge da information_schema.tables (le classi, più elementi), information_schema.columns (le proprietà presenti tra gli elementi di ogni tabella, con il loro genere) e information_schema.key_column_usage (i campi unici). Per i client di PostgreSQL ci sono anche i cataloghi che usano per descrivere uno schema: pg_catalog.pg_class, pg_namespace, pg_attribute, pg_type, pg_tables, pg_views, pg_database, pg_roles, con le classi come tabelle e le proprietà come colonne. Le viste si leggono anche da information_schema.views, i riferimenti tra tabelle da information_schema.foreign_keys. Gli altri cataloghi esistono, vuoti.

Le condizioni in AND del tipo «colonna confronto valore» usano gli indici del motore, e un JOIN su un'uguaglianza tra colonne non prova tutte le coppie. Non ci sono chiavi su più colonne, vincoli CHECK, funzioni usate come tabelle (unnest), né procedure ed estensioni.

Relazioni a partire da una classe

onto
[ "Mario"=nome, V=febbre ] = mario
[ "Luca"=nome, F=febbre ] = luca
add_relation{ mario=from, "contatto"=rel, luca=to }

class { V=febbre } = Febbricitanti
forward{ Febbricitanti=from, "contatto"=rel } = contatti : list

forward e backward accettano come estremo un elemento, una lista di elementi o una classe: nel terzo caso seguono la relazione da tutti i membri correnti e restituiscono gli id raggiunti, senza ripetizioni.

Modifica e ritiro

onto
[ "Mario Rossi"=nome, V=febbre, V=tosse ] = mario

set_property{ mario=element, "febbre"=property, F=value }
mario.febbre : print      # F

retract{ mario=element }

set_property cambia il valore trivalente di una proprietà: l'indice, le classi e gli accessi mario.febbre successivi vedono il nuovo valore. retract ritira l'elemento: il record resta conservato, ma esce da ricerche, classifiche e classi.

Transazioni

onto
[ "Conto A"=nome, V=attivo ] = conto

transaction {
    [ "Bonifico"=causale, 250=importo, V=eseguito ] = movimento
    add_relation{ conto=from, "movimento"=rel, movimento=to }
    set_property{ conto=element, "attivo"=property, V=value }
}

Le scritture del blocco valgono tutte o nessuna. Se un'istruzione fallisce, il motore e le variabili tornano com'erano prima del blocco e l'errore risale. Con stato persistente (--store) le scritture della transazione entrano nel log come una voce sola: dopo un'interruzione, al riavvio ci sono tutte o nessuna. Una transazione dentro un'altra ne fa parte. Per poter annullare, all'ingresso viene tenuta una copia del motore: il costo cresce con la quantità di dati.

Importazione di altri file

import "regole/sintomi.onto"

Esegue un altro file sorgente nell'ambiente corrente: le sue funzioni, variabili, classi ed elementi diventano disponibili da lì in poi. Il percorso è relativo alla directory del file che contiene l'import. Ogni file è eseguito una sola volta, anche se importato più volte o in modo circolare. Non ci sono spazi di nomi separati: l'ambito è globale, come nel resto del linguaggio.

Interpolazione di stringhe

onto
"Mario" = nome : str
[ "Anna Bianchi"=nome, V=febbre ] = paziente

"Ciao {nome}" : print
"{paziente.nome} ha febbre: {paziente.febbre}" : print

{espr} supporta un identificatore o un accesso a un campo (nome.campo), non espressioni arbitrarie.

Funzioni built-in del motore

Espongono OntoEngine dal linguaggio con la sintassi di chiamata nome{valore=arg, ...}:

Funzione Argomenti Risultato
search_val property (str), value (V/F/N, numero o testo) lista di id con quel valore: tra le proprietà trivalenti se value è V/F/N, tra numeri e testi altrimenti
search_range property (str), min e/o max (numeri o testi) lista di id il cui valore è nell'intervallo, estremi inclusi, ordinata per valore
search_text property (str), query (str), match opzionale ("all" o "any") lista di id in ordine di rilevanza: testi che contengono tutte le parole di query, o almeno una con "any" (* finale = prefisso)
synonym word (str), same (str); oppure file (str); oppure thesaurus (str) — (dichiara due parole sinonimi per search_text); con file carica un gruppo di sinonimi per riga, con thesaurus un dizionario dei sinonimi, e restituisce quante voci ha letto
sql query (str) lista di righe, ognuna una lista di valori: il risultato di una SELECT, o gli id creati da una INSERT
text_language language (str); property (str, facoltativo) — (lingua con cui search_text riduce le parole alla radice: di tutti i testi, o di una proprietà)
nearest property (str), vector (lista di numeri), k (int); of e exact (facoltativi) lista degli id dei k elementi con il vettore più vicino, dal più vicino
vector_index property (str) — (dà ai vettori della proprietà un indice dei vicini: nearest non li confronta più tutti, e il risultato è approssimato)
similar_text property (str), query (str), k (int); of (facoltativo) lista degli id dei k elementi con il testo più vicino per significato alla domanda, dal più vicino
unique property (str) — (dichiara unico un campo numerico o testuale)
join left, right; on (str) oppure left_on e right_on; where (funzione) lista di coppie [id_sinistro, id_destro] con lo stesso valore del campo e/o che soddisfano la condizione
sum, avg, min, max of (classe, lista o elemento), property (str) somma, media, minimo o massimo di un campo; senza valori la somma è 0 e gli altri sono N
group of, property (str) lista di [valore, [id, ...]], in ordine di valore
element id l'elemento con quell'id, ricostruito dal motore
set_property element, property (str), value (V/F/N) — (imposta una proprietà trivalente di un elemento già creato)
retract element — (ritira l'elemento: resta conservato ma esce da ricerche, classifiche e classi)
search_profile una coppia valore=prop per ogni proprietà del profilo lista di id (AND esatto su tutte, ordinato per selettività)
top_k k (int) lista di [id, peso], ordinata per peso epistemico = Σ(V=2,N=1,F=0)
distribution property (str) [conteggio_V, conteggio_F, conteggio_N]
add_relation from, rel (str), to — (arco tipato nel grafo integrato; i due estremi devono esistere e non essere ritirati)
forward from, rel (str) lista di id raggiungibili in avanti; from può essere un elemento, una lista o una classe
backward to, rel (str) lista di id che raggiungono to; to può essere un elemento, una lista o una classe
count Classe=class oppure of (classe, lista o elemento) numero di membri correnti della classe, o di elementi
members Classe=class lista degli id dei membri correnti

from/to accettano sia un id intero sia un elemento (ne usa l'id assegnato dal motore all'inserimento). Esempi completi in examples/motore_db.onto (interrogazione e relazioni) e examples/classi.onto (classi e membership).

Errori comuni

  • vero e falso non sono parole chiave. Sono identificatori normali; i valori di verità sono solo V, F, N. V = vero : bool dichiara una variabile di nome vero, non un letterale booleano.
  • Chiamate con parentesi. Le funzioni si invocano solo con nome{valore=arg}; nome(arg) non è previsto dalla grammatica.
  • Aspettarsi che un ramo if giri su N. Su condizione N nessun ramo esegue: per gestire l'indeterminato servono tre controlli (== V, == F e il fall-through), come in interpreta_diagnosi di diagnosi.onto.
  • Interpolare espressioni complesse. {a ^ b} in una stringa non è valido: interpolare un identificatore o un accesso a campo già calcolato.
  • Aggregare valori che non sono proprietà dell'ambiente. aggregate X ▷ a richiede che a sia un nome definito; se non lo è, errore a runtime.
  • Classi cicliche. Un selettore che referenzia la classe stessa, anche indirettamente, produce un errore a runtime.
  • Cercare un testo con un valore di verità. search_val{"nome"=property, V=value} cerca tra le proprietà trivalenti e non trova nulla: per i testi e i numeri il valore va passato com'è, search_val{"nome"=property, "Mario"=value}.
  • Confronti rovesciati nei selettori. In un selettore il confronto ha la proprietà a sinistra: anno >= 1950, non 1950 <= anno.

Fonte: onto-rs/docs/LINGUAGGIO.md