momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Cu Prolog, asupra unor limbaje aritmetice

Prolog
2025 jul

O expresie aritmetică elementară este un "cuvânt" format după anumite reguli (binecunoscute), din simbolurile $\{+,-,*,/,\,(,),\,a,b,c,...\}$ unde prin [$a,b,c,...$] desemnăm un anumit număr de operanzi (numerici, sau literali cu semnificație numerică). Regulile, induse de pe băncile școlii, dau un sens formulelor respective (permițând evaluarea acestora): operatorii $*$ și $/$ au prioritate față de $+$ și $-$; termenii componenți se asociază la stânga, iar parantezele modifică asocierea implicită.
De exemplu, expresia "$10-2-3$" are valoarea 5, iar expresia "$10-(2-3)$" are valoarea 11; expresia "$3+2*3$" are valoarea 9, iar expresia "$(3+2)*3$" are valoarea 15 (în schimb, un operator ca "ridicarea la putere" — ignorat aici — asociază la dreapta: "2^3^2" are valoarea 2^(3^2)=$2^9$ și nu (2^3)^2=$8^2$).
Subliniem că o expresie ca "a*b+c+d" (fără paranteze) are în vedere asocierea implicită la stânga, reprezentând de fapt "(a*b+c)+d".

Un limbaj colectează toate "cuvintele" — sau după caz, propoziții, expresii, programe — care se pot forma pe baza unor reguli gramaticale prestabilite. Limbajul expresiilor aritmetice este finit (numărul de operanzi fiind limitat); anterior, am generat acest limbaj (folosind R, sau într-un alt loc C) pentru cazul a 4 operanzi (angajând însă toate permutările acestora, ceea ce este totuși necuvenit: operanzii sunt termeni inițiali independenți și țin locul unor valori numerice arbitrare).

În Prolog, operatorul "=.." (numit Univ) ne permite să construim expresii binare — în particular sume, produse etc. de câte doi termeni numerici, caz în care putem obține valoarea expresiei prin operatorul "is":

?- E =.. [binop, a, b].
    E = binop(a, b).  % o expresie cu doi operanzi arbitrari
?- E =.. [+, 5, 7], write_canonical(E), V is E.
    +(5,7)   % intern, se folosește forma prefixată ("binop" este "+")
    E = 5+7, % afișarea este în forma obișnuită, infixată
    V = 12.  % evaluarea este posibilă, fiindcă operanzii sunt constante numerice

Mai general să zicem, dacă E1 și E2 sunt două sub-expresii cu câte un anumit număr de operanzi, atunci le putem îmbina printr-un operator binar într-o expresie E = binop(E1, E2) (folosind E:=[binop, E1, E2]), cu forma infixată "E1 binop E2".
Rezultă deja de aici, că regulile după care construim expresii aritmetice — "gramatica" limbajului aritmetic, explicitată ca atare în diverse locuri (v. [1])  — constituie o gramatică de tip TAG, "Tree Adjoining Grammar": expresiile respective sunt modelate prin arbori binari, în care nodurile interioare corespund operatorilor "binop", iar cele terminale corespund operanzilor; parcurgerea în preordine a arborelui dă forma prefixată a expresiei (cum redă write_canonical; v. [2]).

Prin predicatul predefinit append/3 putem despărți recursiv lista inițială a operanzilor, în câte două sub-liste nevide (din care, folosind operatorul "Univ", vom putea compune câte o expresie cu operanzii respectivi); de exemplu, pentru 4 operanzi:

?- append(L1, L2, [a,b,c,d]), L1 \= [], L2 \= [].
L1 = [a],
L2 = [b, c, d] ;   % E = binop([a], [b,c,d])
L1 = [a, b],
L2 = [c, d] ;      % E = binop([a,b], [c,d])
L1 = [a, b, c],
L2 = [d] ;         % E = binop([a,b,c], [d])
false.

Întregind cele de mai sus, următoarea procedură Prolog (recursivă) ne va permite să obținem limbajul corespunzător listei Lop a operanzilor inițiali:

arth_tree([X], X).  % cazul terminal, când avem un singur operand
arth_tree(Lop, Expr) :-
    append(L1, L2, Lop), L1 \= [], L2 \= [],
    arth_tree(L1, E1),  % construiește (recursiv) sub-expresia cu operanzii din L1
    arth_tree(L2, E2),  % și subexpresia cu operanzii din L2
    member(Op, [+, -, *, /]),  % "binop"
    Expr =.. [Op, E1, E2].  % îmbină sub-expresiile, prin "binop"

Exemplificăm cazul cel mai simplu — limbajul aritmetic cu doi operanzi:

?- arth_tree([a,b], E).  % arth_tree([a,b,c], E). pentru 3 operanzi, etc.
E = a+b ;
E = a-b ;
E = a*b ;
E = a/b ;
false.  % cele 4 expresii cu doi operanzi

O expresie cu trei operanzi implică doi operatori (posibil identici) dintre cei 4 și poate avea două forme (putem paranteza primii doi, respectiv ultimii doi operanzi) — deci limbajul aritmetic cu trei operanzi conține $4^2\times 2$ = 32 de expresii. O expresie cu patru operanzi implică 3 operatori și poate avea 5 forme (v. [2]), deci limbajul corespunzător are lungimea $4^3\times 5$ = 320. Pentru 5 operanzi, ținând seama că avem 14 forme posibile (v. Catalan number), rezultă $4^4\times 14$ = 3584 expresii aritmetice distincte sintactic.
De sesizat că operanzii contează ca număr, dar și ca ordine: schimbând ordinea lor… obținem un alt limbaj; însă două limbaje aritmetice care diferă numai prin ordinea inițială a operanzilor, sunt "izomorfe": expresiile unuia devin expresiile celuilalt aplicându-le o aceeași permutare de operanzi.

Folosind arth_tree sub findall/3, prin următorul predicat putem obține lista tuturor expresiilor distincte din punct de vedere sintactic (cu o listă inițială de operanzi):

lang(Operands, Lang) :-
    findall(Expr, arth_tree(Operands, Expr), Lang).

Introducând în prealabil o procedură de evaluare a unei expresii, putem obține prin maplist/3, valorile tuturor expresiilor respective:

eval_expr(E, V) :- V is E.  % E având ca operanzi, constante numerice
?- lang([3,4,6,7], Lang), maplist(eval_expr, Lang, V).
Lang = [3+(4+(6+7)), 3-(4+(6+7)), 3*(4+(6+7)), 3/(4+(6+7)), 3+(4-(6+7)), 3-(4-(6+7)), 3*(4-(... + ...)), 3/(... - ...), ... + ...|...],
V = [20, -14, 51, 0.17647058823529413, -6, 12, -27, -0.3333333333333333, 55|...].

Dar desigur, în loc să explicităm cumva lista trunchiată afișată pe ecran, mai bine scriem rezultatul într-un fișier-text (pe care îl vom putea investiga și din afară):

?- open('3467.txt', write, Stream), 
   forall(lang([3,4,6,7], L), write(Stream, L)), 
   write(Stream, '.'), close(Stream).

Rezultatul este înscris ca listă, [[3+(4+(6+7)),3-(4+(6+7)), ...], dar a trebuit să adăugăm '.' după paranteza finală — fiindcă read/2 pretinde ca termenii pe care îi citește din fișier (aici, o listă Prolog) să aibă "full stop" (adică '.') la capăt.

Să aflăm de exemplu, care expresii cu operanzii 3, 4, 6 și 7 au valoarea 23:

?- S = 23,  % instanțiază variabila S pe valoarea dorită
|    open('3467.txt', read, Stream), read(Stream, L),
|    member(H,L), S is H.  % extrage câte o expresie și o evaluează
S = 23,
Stream = <stream>(0x5ab3688385e0),  % pointează fișierul din care citește expresiile
L = [3+(4+(6+7)), 3-(4+(6+7)), 3*(4+(6+7)), 3/(4+(6+7)), 3+(4-(6+7)), 3-(4-(6+7)), 3*(4-(... + ...)), 3/(... - ...), ... + ...|...],  % se afișează lista citită din fișier
H = 3*(4+6)-7 ;  % o primă expresie cu valoarea S; prin ';' cerem alta
false.  % nu există o altă expresie decât "3*(4+6)-7", cu valoarea 23...

În schimb, cu S = 24 obținem direct "false." — adică nu există expresii de valoare 24, pentru operanzii 3, 4, 6 și 7… în această ordine!

Este de subliniat această nuanță: am considerat operanzii numai în ordinea fixată în lista furnizată inițial a acestora (spre deosebire de programul R din [2], unde implicam toate permutările operanzilor); schimbând ordinea operanzilor, prin lang([4,6,7,3],L) de exemplu, se va genera un alt set de 320 expresii decât cel rezultat mai sus — set în care putem avea și alte expresii cu valoarea 23: "(4/6+7)*3"; iar lang([7,3,6,4],L) de exemplu, conține încă o expresie de valoare 23: "7*3+6-4".

Să observăm totuși că având într-un fișier setul de 320 expresii rezultat prin lang([a,b,c,d],L1), putem înlocui peste tot 'a' și 'b' cu 'b' și 'a' de exemplu — rezultând setul care s-ar fi obținut prin lang([b,a,c,d],L2) (analog, pentru oricare altă permutare de operanzi). Deci nu prea este cazul de a genera deodată, toate cele 320×4!=7680 expresii posibile cu 4 operanzi: din setul de bază, cu 320 expresii, se obține ușor oricare altul (corespunzător unei permutări a operanzilor), aplicând o transformare elementară de substituire (specifică lucrului uzual cu "expresii regulate"). Astfel, devine fezabilă și investigarea expresiilor cu mai mult de 5 operanzi (pentru 5 operanzi de exemplu, generăm 3584 expresii "de bază", în loc de toate cele 3584×5!=430080 formate prin permutările de operanzi).

vezi Cărţile mele (de programare)

docerpro | Prev |