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)