Упражнение 4.
Аритметика, отсичане и отрицание.

1.Аритметика. В Пролог, както вече говорихме, има константи- числа, които се изписват по обичайния начин. За работа с числа има следните вградени предикати, които имат инфиксен запис:
    = - това равенство е малко по особено, то се нарича равенство по унифицируемост, т. е. то може да се прилага не само към числа, но и към променливи и структури. Първо се опитва да унифицира лявата и дясната част и ако успее, отговаря с yes, ako не успее, отговаря с no, като ако при тази унификация някоя променлива се остойности или съвмести, тя си остава такава.
     =:= - това е тъй нареченото силно равенство, то успява само в случай, че от ляво и от дясно стоят равни числа, като при това от някоя страна, ако стои остойностена променлива, тя се замества с нейната стойност, ако стои аритметичен израз, той се пресмята. Внимание: ако някъде има свободна променлива, ще пропадне.
     \= - това е съответното неравенство на равенството по унифицируемост, то успява, ако = пропадне и обратното. Тук не настъпват никакви остойностявания на променливите.
     =\= - това е съответното неравенство на =:=, пресмята изразите от двете страни и ги сравнява.
     <,>,=<,>= - имат ясен смисъл, като отново ако от ляво или от дясно стои израз, той първо гo пресмята и после ги сравнява. Като при това има следната особеност в SP, ако в някоя от страните стои единствено неостойностена променлива, това дава yes, защото в дефиницията им е
използвано =. Ако тази променлива е в израз, вече работи ОК, но това е в случая с =, ако е строго вече дава no  при свободна променлива от едната страна, така че  трябва да се предупредят да използват тези предикати само със остойностени променливи. Също така трябва да се каже, че стандартът изисква тези предикати да работят само с числа, т. е. в стандарта ако някъде има израз, пропада.
     is -  Това е Прологовото присвояване, по правило от ляво трябва да стои променлива, а от дясно числов израз, който може да се пресметне, т. е. променливите в него трябва да са свързани; Пролог пресмята стойността на израза и остойностява променливата от ляво с нея и успява, ако тази променлива е свободна, а ако не е, я сравнява с нейната стойност. В SP това работи и наобратно, т. е. ако от дясно е променливата, а от ляво изразът, успява когато и от двете страни са свободни променливи, като при това ги съвместява,
ако вече в някоя от страните има свободна променлива, която участва в израз, не успява. Общо взето, принципът явно е: смята и ако не може да сметне нещо, дава no, иначе продължава. Какви функционални символи могат да съдържат изразите, трябва да се помни, че стандартът изисква от Пролог да поддържа цели
числа и целочислена аритметика, така че във всеки Пролог по някакъв начин са реализирани:
 +  събиране
 -  изваждане, когато искаме в SP да го използваме като унарен минус, трябва да заграждаме аргумента в скоби.
 *  умножение
 // целочислено деление
 mod  деление по модул на практика работи като остатък
 rem  остатък
В SP има допълнително:
 **  степенуване
 abs абсолютна стойност естествено, тъй като това и следващото е унарен символ, то те имат префиксен запис и аргументът се загражда в скоби
 sign знак.
В SP има и аритметика с плаваща точка; ако им потрябва нещо, могат да погледнат в help-а и да го използват.

Задачи:
1. Да се направи предикат, който при преудовлетворяване да дава всички естествени числа в нарастващ ред:

int(0).
int(N):-int(N1), N is N1 + 1.

Особености:
По принцип може да се докаже и е направено, че езикът на логическото програмиране е най- мощен в смисъл, че най-много задачи могат да се решат с него сред езиците за програмиране, ако работим с произволни абстрактни типове данни и не се интересуваме от представянето, или по- скоро не го знаем, на елементите на домеина. На практика тъй като ние представяме елементите с подходящо кодиране чрез естествени числа, то изразителната мощ на езиците за логическо програмиране е равна на останалите езици за структурно програмиране, например C, Pascal и т. н. Това вероятно ще се обсъжда по-подробно след година в курса по
Семантика на езици за програмиране. Тази особеност на езиците за логическо програмиране отчасти се дължи на факта, че в тях можем да създадем генератори за някакво множество и впоследствие, като използваме някакъв тестващ предикат, да отделим тези елементи, които ни трябват. Т. е да "търсим" по някакво множество, без да се налага да отговаряме на въпроса кога един елемент не е от множеството, което в много случаи
не може да се реши алгоритмично (т. е. може да се докаже че няма алгоритъм, който решава този въпрос). По- подробно с тези проблеми ще се занимава изборният курс по Изчислимост и сложност догодина. Това е характерна особеност на ЛП, която го прави много удобен за решаването на редица задачи, още повече, че за да се решат повечето от тези задачи дори и при структурното програмиране се налага да се моделира механизмът
за извод в Пролог, т. е. от гледна точка на бързина нищо не може да се подобри. Сега ще упражним точно това, естествено не всички примери са толкова сложни, че ако се реализират на C да речем няма по- ефективни програми, те са по-скоро подбрани с учебна цел.

2. Да се напише предикат, който генерира четните числа в растящ ред:

even(X):-int(X), 0 =:= X mod 2.
 

3. Да се напише предикат, който да е на два аргумента и при първи аргумент число x, а втория свободна променлива да успява и да връща във втория x!.

fak(0,1).
fak(X,Y):-T is X - 1,fak(T,Z),Y is X * Z.

А сега, ако искаме да го използваме и като разпознавател какво трябва да променим:

fak(0,1).
fak(X,Y):-X >0,T is X - 1,fak(T,Z),Y is X * Z.

4. Да се напише генератор, който да генерира при преудовлетворяване всички числа в даден интервал.

Забележка: Ще работим само с цели числа, затова няма да го указваме изрично.

between(X,Y,X):-X =< Y.
between(X,Y,Z):-X < Y,X1 is X + 1,between(X1,Y,Z).

5. Ясно е, че горната дефиниция на факториел може да връща само стойност при зададено x, но ако искаме обратното, т.е. при зададено число z да намира такова x, че x!=z и да отговаря с no ако няма такова какво трябва да направим:

ofak(X,Y):-between(0,Y,X),fak(X,Y).

Задачи за практикума:

1. Да се напише предикат на два аргумента, който при всевъзможните преудовлетворявания да генерира наредени двойки от естествени числа (n,k), по нарастване на n,  така че сумата на числата от 1 до n да е равна на сумата на числата от n до n+k.

tupo(X,Y):-int(X),between(0,X,Y),S1 is (X * (X + 1)) // 2,
           S2 is (((2 * X) + Y) * (Y + 1)) // 2,S1 = S2.

Предикати за управление:
Отсичане- това е вграден предикат, който синтактично изглежда по следния начин: cut или за по- кратко !, който винаги се удовлетворява, но при опит за преудовлетворяване пропада. Извикан от тялото на някое правило, той означава "зарежи" възможните алтернативни решения, защото се интересуваме само дали можем да минем по точно този път.
 
Т. е. когато в програмата се срещне отсичане, то то "отсича" пътя, който представлява редицата на доказателствата, така че следващата цел се съединява с предишната, или ако си представим дървото на извод то залепя всички възли от дясно, като непосредствени наследници на родителския възел на целта, от която е извикано. Или казано накратко, маха предшественика и всичките му наследници от ляво. Като при това когато Пролог срешне !, той се "сеща" как точно ще продължи механизмът на възврат и за тези цели, които не могат да се преудовлетворяват не пази маркери, т. е. с ! освен че можем да ускорим програмата, защото режем някои пътища, пестим и памет. Случаи, в които може да се използва !:
-ако сме сигурни, че възможните преудовлетворявания няма да доведат до получаването на нещо по-
добро. Например  предикатът  member, ако преудовлетворяваме, ще получаваме yes толкова пъти, колкото срещания имаме. Ако искаме просто еднократно да ни казва дали даден елемент се среща или не в даден списък, трябва да го запишем така:

member(X,[X|_]):-!.
member(X,[_|L]):-member(X, L).

-ако искаме да фиксираме пътя, по който трябва да минем. Например ако искаме да реализираме следната конструкция:

if a then b else c.

if:-a,!,b.
if:-c.

-в комбинация с fail. fail е вграден предикат, който никога не успява. Тази конструкция се използва в случай, че искаме да окажем, че попаднем ли в дадена ситуация, непременно трябва да имаме отговор no. Например, ако искаме да подобрим работата на генератора between, като не правим толкова много проверки от вида X<Y, то можем да постъпим така:

betw(X,Y,Z) :- X > Y,!,fail.
betw(X,Y,X).
betw(X,Y,Z) :- X1 is X + 1, betw(X1,Y,Z).

Трябва да се внимава при употребата на !. Наистина той твърде много ускорява работата на програмата, но за съжаление доста усложнява разчитането й, защото ние вече не работим чисто декларативно, което прави трудно откриването на евентуални грешки. За съжаление в Пролог, за разлика от чистото логическо програмиране, употребата на ! не може да се избегне.

call- това е предикат на един аргумент, който трябва да бъде цел за Пролог и се удовлетворява, ако целта се
удовлетворява, иначе пропада.

not- това е предикат отново на един аргумент- цел за Пролог, който пропада, ако целта се удовлетворява, и успява иначе. Внимание: това не е традиционното логическо отрицание, ако на not  му подадем цел, при чието изпълнение попадаме в безкраен цикъл, тогава и not  ще зацикли, а няма да върне yes. Как се реализира not в Пролог:

not(C):-call(C),!,fail.
not(C).
Когато не сме сигурни как ще отговори not, най-добре е да си спомним дефиницията му.

repeat- за разлика от ! този предикат винаги се удовлетворява и преудовлетворява. Той служи за правенето на
цикъл. Каква му е дефиницията:

repeat.
repeat:-repeat.
 
 

 
Задача за практикума:
1. Да се направи генератор за съвършени числа. Едно число е съвършено, ако е равно на сумата от делителите си.

perfect(X):-int(X), is_per(X).

is_per(X):-X > 1,sum(X,Y), X = Y,!.

sumdel(_,1,1):-!.
sumdel(X,Y,R):-Y > 0,0 =:= X mod Y,!,
Y1 is Y - 1,sumdel(X,Y1,R1), R is R1 + Y.
sumdel(X,Y,R):-Y > 0, Y1 is Y - 1,sumdel(X,Y1,R).

sum(X,R):-X1 is X - 1,sumdel(X,X1,R).

2. Да се направи генератор на прости числа.

is_p(X,1):-!.
is_p(X,Y):-0 =:= X mod Y,!,fail.
is_p(X,Y):-Z is Y - 1,is_p(X,Z).

is_prime(X):-X > 1,Y is X - 1,is_p(X,Y),!.

prime(X):-int(X),is_prime(X).
 

3. Да се направи генератор на питагорови тройки. Една наредена тройка от числа е питагорова, ако сумата от
квадратите на първите две е равна на третото на квадрат.

is_int(1).
is_int(X) :- is_int(X1), X is X1 + 1.

sort3([X,Y,Z]) :- is_int(X),
                  betw(1,X,Y),
                  betw(1,Y,Z).

list_3(L) :- sort3(T),
             perm(T,L).

pt(X,Y,Z):-list_3([X, Y, Z]),Z*Z =:= X*X + Y*Y.

4. Напишете генератор за числата на Фибуначи.

fib(0,1):-!.
fib(1,1):-!.
fib(N,X):-Y is N - 1,Z is N - 2,fib(Y,Y1),
  fib(Z,Z1),X is Z1 + Y1.
fib1(X):-int(N),fib(N,X).