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 daV ^ 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 daV <=> V, N se un operando è N, altrimenti F.F <=> F = F: la coimplicazione non è un'uguaglianza.xor, anche⊕: mai V; N daF xor FeN 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.
"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.
[ "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.
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
[ "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:
[ "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
[ "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
[ "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
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" = 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
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
[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 ▷
"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:
[ "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à:
[ "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
[ "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.
[ "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).
[ "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.
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).
[ "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
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
[ "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.
[ "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
[ "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.
[ "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:
[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,MAXdi 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 ammettonoBETWEEN,LIKE(%e_),~e~*(espressioni regolari),IN (valori),IN (SELECT ...),EXISTS (SELECT ...),IS [NOT] NULL,AND,OR,NOTe 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(ancheTRUE,FALSE,NULL):attivo = Ntrova chi ha quella proprietà indeterminata. Un confronto con una colonna che l'elemento non ha non è vero;IS NULLtrova proprio chi non ce l'ha. ORDER BYaccetta colonne, aggregati, nomi dati conASe posizioni (ORDER BY 2 DESC).INSERTcrea 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 conCREATE TABLE.UPDATEimposta valori di verità, numeri e testi;DELETEritira gli elementi.- Una lista si scrive
ARRAY[1, 2, 3]o si ricava da un'interrogazione conARRAY(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_ARRAYfa 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, chenearestsa 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é valereN;DEFAULT valore: il valore di chi non nomina la colonna;PRIMARY KEYeUNIQUE: 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
[ "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
[ "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
[ "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
"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
veroefalsonon sono parole chiave. Sono identificatori normali; i valori di verità sono soloV,F,N.V = vero : booldichiara una variabile di nomevero, 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
ifgiri su N. Su condizione N nessun ramo esegue: per gestire l'indeterminato servono tre controlli (== V,== Fe il fall-through), come ininterpreta_diagnosididiagnosi.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 ▷ arichiede cheasia 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, non1950 <= anno.