עקרונות שפות תכנות אביב 2102. תרגול 00. תכנות לוגי חלק ב' נושאים: רשימות. 1. אופטימיזציית ה- backtracking אופרטור קאט. 2. Meta circular interpreter..3 0. רשימות. רשימות בפרולוג מיוצגות בצורה הבאה: ][ רשימה ריקה. f(y)[,x[,2 רשימה בת שלושה איברים: משתנה X, מספר 2, ו- term המורכב מפנקטור f ומשתנה Y. [X Xs] ביטוי דמוי."cons" X הוא האיבר הראשון ברשימה, Xs משתנה המייצג רשימה הכוללת את שאר האיברים. כלומר, הרשימה 5[ ]3, יכולה להראות כמו ] ] ] [ [5.[3 s --> np vp np --> det n vp --> v np v det --> a the n --> woman man v --> shoots דוגמה 0. CFG הממומש בפרולוג. בדוגמה הזאת נראה איך ליצור משפטים של.CFG שאלה: אילו סוגי משפטים ניתן להרכיב עם הדקדוק הזה? s(z) :- np(x), vp(y), append(x, Y,Z). נתרגם את הדקדוק לפרולוג בצורה הבאה: np(z) :- det(x), n(y), append(x, Y, Z). vp(z) :- v(x), np(y), append(x, Y, Z). vp(z) :- v(z). det([a]). det([the]). n([woman]). n([man]). v([shoots]). ההתאמה בין CFG לפרולוג צריכה להיות ברורה. -1-
למשל,?- s([a, woman, shoots, a, man]). כדי להשתמש בתוכנית לגזירה, ניצור שאילתה מתאימה. למעשה, קיבלנו generator שדיברנו עליו בתרגול הקודם. אנחנו יכולים ליצור את כל המשפטים האפשריים שתואמים את הדקדוק, סה"כ 22 משפטים. הנה חמשת הראשונים:?-s(X). X = [the, woman, shoots, the, woman] ; X = [the, woman, shoots, the, man] ; X = [the, woman, shoots, a, woman] ; X = [the, woman, shoots, a, man] ; X = [the, woman, shoots];?- s([the, man X]). X = [shoots, the, woman] ; X = [shoots, the, man] ; X = [shoots, a, woman] ; שאלה: נניח שנרצה להוסיף עוד כמה כללים לדקדוק )השינויים מודגשים בקו תחתון(: s --> np vp np --> det n det adj n vp --> v np v det --> a the n --> woman man v --> shoots adj --> vicious marvelous np(z) :- det(x), adj(w), n(y), append([x, W, Y], Z). מה צריך לשנות בקוד? נוסיף את הקוד הבא: adj([vicious]). adj([marvelous]). -2-
דוגמה 2: תאריכים. נגדיר יחס בין תאריכים כדי שנוכל להשוות ביניהם. נתאר תאריך כיום בשבוע ושעה. נגדיר תחילה את כל האפשרויות כעובדות בעזרת רשימות: week_day(['sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']). hour([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]). נראה כי שעה 2 לפני שעה 9. מכיוון שאנחנו בתכנות לוגי, הכל נעשה בעזרת יחסים. אין יחס )9,2(< כי אי אפשר להגדיר אותו נצטרך אינסוף עובדות בשביל כל המספרים האפשריים. לכן נעשה את זה באמצעות בדיקה של איזה מספר מהשניים נמצא קודם ברשימת השעות. )פרולוג, לעומת תכנות לוגי, אכן כולל יחסים בין מספרים בפרט ומתמטיקה בכלל, כי הוא מאחד בתוכו גם את התכנות הלוגי וגם מאפיינים של שפת תכנות רגילה.( date([h, W]) :- hour(hour_list), member(h, Hour_list), week_day(weekday_list), member(w, Weekday_list). נגדיר כלל שבודק את חוקיות התאריך: לפיכך, להשוואת התאריכים: datelt(date([_, W1]), date([_, W2])) :- week_day(weekday_list), precedes(w1, W2, Weekday_list). datelt(date([h1, W]), date([h2, W]) ) :- hour(hour_list), precedes(h1, H2, Hour_list). היחס precedes בודק האם הפרמטר הראשון נמצא לפני הפרמטר השני ברשימה. דוגמאות הרצה:?- date([1, 'Sun']).?- datelt(date([5, 'Mon']), date([1, 'Tue'])). precedes(x, Y, Z) :- append([_, [X], _, [Y], _], Z). הקוד של :precedes השתמשנו בפרדיקט.append/2 הוא לוקח רשימה של רשימות ומחבר את כולן. הפרדיקט שבנינו חזק מאוד, כי נוכל להשתמש ב- matching patterns כדי האפשרויות. כל את לקבל -3-
דוגמה 3 )חומר השלמה(. נכתוב פרוצדורה שמקבלת רשימה ואיבר בה ומורידה הופעה אחת של האיבר ברשימה. % Signature: select (X, HasXs, OneLessXs)/3 % Purpose: The list OneLessXs is the result of removing % one occurrence of X from the list HasXs. select(x, [X Xs], Xs). select(x, [Y Ys], [Y Zs]) :- select(x, Ys, Zs).?- select(4, [2, 3, 2, 4, 5, 2, 4], X). X = [2, 3, 2, 5, 2, 4] ; X = [2, 3, 2, 4, 5, 2] ; false דוגמה 4. נכתוב פרוצדורה שמקבלת רשימה ואיבר בה ומורידה את כל ההופעות של האיבר ברשימה. % Signature: delete(list, X, HasNoXs)/3 % Purpose: The list HasNoXs is the result of removing all % occurrences of X from the list List. delete([ ], _, [ ]). delete([x Xs], Z, [X Ys]) :- X \= Z, delete(xs, Z, Ys). delete([x Xs], X, Ys) :- delete(xs, X, Ys).?-delete([2, 3, 2, 4, 5, 2, 4], 2, X). X = [3,4,5,4] =\ הינו סימן הפוך מסימן =. פירושו "לא ניתן לבצע יוניפיקציה". דוגמה 5. מיזוג שתי רשימות. ניצור פרדיקט שבודק האם מיזוג של שני הפרמטרים כפילויות, הן תופענה גם בתוצאה הראשונים נותן פרמטר שלישי. אם ישנם % Signature: merge(xs, Ys, Zs)/3 % Purpose: Zs is an ordered list of dates obtained % by merging the ordered lists of dates Xs and Ys. merge([x Xs], [Y Ys], [X Zs]) :- datelt(x, Y), merge(xs, [Y Ys], Zs). % 1 merge([x Xs], [X Ys], [X, X Zs]) :- merge(xs, Ys, Zs). % 2 merge([x Xs], [Y Ys], [Y Zs]) :- datelt(y, X), merge([x Xs], Ys, Zs). % 3 merge(xs, [ ], Xs). % 4 merge([ ], Ys, Ys). % 5 אין באפשרותנו למזג רשימות מספרים, כי לא נוכל להשוות ביניהם.?- merge([date([5, 'Sun']), date([5, 'Mon'])], X, [date([2, 'Sun']), date([5, 'Sun']), date([5, 'Mon'])]). -4-
X = [date([2, 'Sun'])]?- merge([date([1, 'Sun']), date([3, 'Wed']), date([5, 'Sat'])], [date([2, 'Sun']), date([3, 'Wed'])], Xs). Xs = [date([1,'sun']), date([2,'sun']), date([3,'wed']), date([3,'wed']), date([5, 'Sat'])] ; false חומר השלמה: לשם דוגמה נסמן: d1 = date([1, 'Sun']) d2 = date([2, 'Sun']) d3 = date([3, 'Wed']) d5 = date([5, 'Sat']) merge([d1, d3, d5], [d2, d3], Xs) {X_1 = d1, Xs_1 = [d3, d5], Y_1 = d2, Ys_1 = [d3], datelt(d1, d2), Xs = [d1 Zs_1]} merge([d3, d5], [d2, d3], Zs_1) * merge([d3, d5], [d2, d3], Zs_1) Rule 2, Rule 3, Fact 4, Fact 5 failure branches { X_2 = d3, Xs_2 = [d5], Y_2 = d2, Ys_2 = [d3], Zs_1 = [d2 Zs_2]} Rule 3 datelt(d2, d3), merge([d3, d5], [d3],zs_2) * merge([d3, d5], [d3], Zs_2) { X_3 = d3, Xs_3 = [d5], Ys_3 = [ ], Zs_2 = [d3, d3 Zs_3]} Rule 2 merge([d5], [ ], Zs_3) { Xs_4 = [d5], Zs_3 = [d5]} Fact 4 נבנה את התשובה מהענף המוצלח: Xs = [d1 Zs_1] Xs = [d1 [d2 Zs_2]] Xs = [d1 [d2 [d3, d3 Zs_3]]] Xs = [d1 [d2 [d3, d3 [d5]]]] = [d1, d2, d3, d3, d5]. -5-
דוגמה 6 )חומר השלמה(. נכתוב פרוצדורה שמחליפה את כל ההופעות של איבר From להיות To בעץ בינארי. 2 3 2 void void void void % Signature: substitute(from, To, TreeFrom, TreeTo) /4 % Purpose: The binary tree TreeTo is the result of replacing all occurrences of From % by To in the binary tree TreeFrom. substitute(_, _, void, void). substitute(from, To, tree(node, Lft, Rht), tree(node1, Lft1, Rht1)) :- replace(from, To, Node, Node1), substitute(from, To, Lft, Lft1), substitute(from, To, Rht, Rht1). % Signature: replace(from, To, NodeF, NodeT) /4 % Purpose: if (NodeF = From) then (NodeT will be To) else (NodeT will be NodeF). replace(from, To, From, To). replace(from, _, NodeF, NodeF) :- From \= NodeF.?- substitute(2, 4, tree(2, tree(3, void, void), tree(2, void, void)), X). X = tree(4, tree(3, void, void), tree(4, void, void))?- substitute(b, X, tree(a, tree(b, void, void), tree(c, tree(b, void, void), void)), tree(a, tree(c, void, void), tree(c, tree(c, void, void), void))). X = c אם נרצה להציג עץ בינארי כרשימה, נעשה את השינויים הבאים: העץ במקום Right) tree(node, Left, ייראה בתור Right] void,[node, Left, יהפוך להיות [.] אם נרצה עץ n -ארי ולא בינארי, נחליף את הקטע בקוד Right) (Left, ב- Children רשימה של כל הילדים. נצטרך להוסיף פרוצדורה למעבר על העץ. -6-
2. אופטימיזציית ה- backtracking באמצעות אופרטור קאט. פרולוג מספק לנו פרדיקט המערכת, cut )מצוין באמצעות הסימן!(, המאפשר להשפיע על ההתנהגות של התוכניות. פעולתו היא להקטין את מרחב החיפוש של חישובי פרולוג באמצעות גזירת עץ ההוכחה בזמן ריצה. משתמשים בו כדי למנוע מפרולוג לבצע חישובים שהמתכנת יודע שאין בהם שום טעם. דוגמה 7. נחזור למיזוג רשימות בדוגמה 5. מדובר בפעולה דטרמיניסטית: מתוך חמשת הכללים מתאים )יסתיים עם עלה הצלחה( לכל היותר אחד עבור כל קדקוד בעץ ההוכחה. בפרט, כשמשווים שני תאריכים,Y,X רק אחת מהאפשרויות Y) datelt(y, X),X = Y,dateLT(X, יכולה להיות נכונה. ברגע שמצאנו אותה, אין טעם לבדוק את השאר. לכן נחתוך את הבחירות הנוספות באמצעות קאט. כלל 1 ייראה כך: merge([x Xs], [Y Ys], [X Zs]) :- datelt(x, Y),!, merge(xs, [Y Ys], Zs). merge([d1, d3, d5], [d2, d3], Xs) datelt(d1, d2),!, merge([d3, d5], [d2, d3], Zs_1) Rule 3 failure branch!, merge([d3, d5], [d2, d3],zs_1) Rule 2 failure branch merge([d3, d5], [d2, d3], Zs_1) בהתאם, ניתן לשנות כללים נוספים כדי למנוע חיפוש מיותר. השפעת הקאט: 1. לחתוך את הענפים של כל הכללים של הפרדיקט )A בדוגמה( מתחת לכלל הנוכחי בתוכנית. היעד שעבר בהצלחה יוניפיקציה עם כלל הכולל קאט לא ימשיך לייצר פתרונות עם הכללים מתחת לנוכחי בפרדיקט. )לדוגמה: A :-....... A :-...,!,.... A :-.... A :-.... ברגע שהגענו לקאט לא יעשה שימוש בשתי השורות האחרונות.( 2. לחתוך את הענפים עבור כל הפתרונות שנובעים מהיעדים משמאל לקאט. הפתרון יישאר A :- B1,..., Bm,!, C1,..., Cp יחיד. )לדוגמה: ברגע שהגענו לקאט לא תיבדקנה אפשרויות נוספות עבור כל Bi מעבר למה שכבר נבחר.( 3. אבל הקאט לא ישפיע על היעדים שמופיעים אחריו. הם ימשיכו לייצר פתרונות כרגיל. )בדוגמה הקודמת ייבדקו כל האפשרויות של ).Ci 4. במידה שאחד מתוך Ci ייכשל בכל הענפים, יוכרז כישלון לכל A, והחיפוש יימשך רמה אחת מעליו. )לדוגמה, S :-..., G, A,... תיבדק אפשרות נוספת עבור G.( -7-
הערה: קיימים שני סוגים עיקריים של קאט: אדום וירוק. ירוק עוזר לאופטימיזציה של תוכנית בכך שהוא חוסך בזמן ריצה. קאט אדום, לעומת זאת, מונע מפתרונות להיווצר, ויש להיזהר בכתיבת הקוד מתופעה כזאת, במידה ואינכם מעוניינים במפורש שזה מה שיקרה )כמו, למשל, כשאתם נדרשים לספק רק פתרון יחיד(. בדוגמת מיזוג הרשימות ראינו קאט ירוק. Rule k: A :- B 1,, B m,!, C 1,, C p. Rule k+1: A :-. נמצא הקאט נחתכות: גזירת העץ: עבור הפרדיקט כל האפשרויות לגזירה בקדקוד שבו?- G, A, Q 1,, Q n Rule k?- A, Q 1,, Q n?- B 1,, B m,!, C 1,, C p, Q 1,, Q n... *...?-!, C 1, C p, Q 1,, Q n כאן נחתכות אפשרויות לפני הקאט כאן נחתכות בחירות נוספות של A?- C 1,, C p, Q 1, Q n?- merge([ ], [ ], X). דוגמה 8. כמה פתרונות יש לשאילתה X = [ ]; X = [ ]; false הסיבה לתשובה כפולה היא התאמת השאילתה גם לעובדה 4, נוסיף קאט לעובדה 4 נהפוך אותה לכלל. גם לעובדה 5. כדי להימנע מכך, merge(xs, [ ], Xs) :-!. זאת דוגמה פשוטה לקאט אדום מספר הפתרונות השתנה. -8-
Meta-circular interpreter בתכנות לוגי..3 נזכר בתיאור האינטרפרטר בתכנות לוגי. הוא מושתת על יוניפיקציה ו- backtracking. א. בחירת היעד )בפרולוג השמאלי ביותר(. ב. בחירת הכלל עבור היעד )פרולוג שאתם עובדים איתו בוחר את הראשון, ואז ממשיך לפי הסדר במקרה של כישלון או בקשה לתשובה נוספת(. האינטרפרטר מבוסס על זיהוי הסינטקס של term םי- ונוסחאות אטומיות: נוסחאות אטומיות נקראות כ- term -ים באינטרפרטר. אפשר להיזכר ב- interpreter meta circular ב- Scheme, שמזהה את הסינטקס של Scheme לפי צורת ההדפסה של רשימות. נסתכל שוב על האינטרפרטרים שראיתם בכיתה. הגרסה הראשונה אינטרפרטר מבוסס על רדוקציית היעדים: % Signature: solve(goal)/1 % Purpose: Goal is if it is when posed to the original program P. solve() :-!. solve( (A, B) ) :- solve(a), solve(b). solve(a) :- clause(a, B), solve(b). A שעבור כלל שראשו,clause(A, Body) a. b :- a. c(3) :- b. c(8).?- clause(a, X). X =.?- clause(b, X). X = a.?- clause(c(3), X). X = b.?- clause(c(z), X). Z = 3, X = b; Z = 8, X = % Signature: parent(father, Son)/2 parent(abraham, isaac). parent(isaac, jacob). האינטרפרטר מבוסס על פרדיקט מובנה של מחזיר את הגוף שלו, לדוגמה: דוגמה 9. התוכנית הפרולוג % Signature: ancestor(ancestor, Descendant)/2 ancestor(x, Y):- parent(x, Y). ancestor(x, Z):- parent(x, Y), ancestor(y, Z). -9-
?- solve(ancestor(abraham, P)). עץ הוכחה חלקי: {A_1 = ancestor(abraham, P)} Rule 3 solve(ancestor(abraham, P)) clause(ancestor(abraham, P), B_1), solve(b_1) {B_1 = parent(abraham, P), X_2 = abraham, Y_2 = P} {A_3 = parent(abraham, P)} Rule 3 {P = isaac, B_3 =} Fact 1 solve(parent(abraham, P)) clause(parent(abraham, P), B_3), solve(b_3). {B_1 = parent(abraham,y_2), ancestor(y_2, P)} Rule 2 solve((parent(abraham,y_2), ancestor(y_2, P))) {A_3 = parent(abraham, Y_2) B_3 = ancestor(y_2, P)} Rule 2 solve(parent(abraham, Y_2)), solve(ancestor(y_2, P)) {A_4 = parent(abraham, Y_2)} Rule 3 Fact 1 solve()! {P = isaac} clause(parent(abraham, Y_2), B_4), solve(b_4) solve(ancestor(y_2, P)) {Y_2 = isaac, B_4 = } Fact 1 solve(), solve(ancestor(isaac, P)) נרכיב את התשובה )הסימן פירושו בחר רק את אלו שתואמים את המשתנים אחרי הסימן(: {A_1 = ancestor(abraham, P)} {B_1 = parent(abraham, P), X_2 = abraham, Y_2 = P } {A_3 = parent(abraham, P)} {P = isaac, B_3 = } P = {A_1 = ancestor(abraham, isaac), B_1 = parent(abraham, isaac), X_2 = abraham, Y_2 = isaac, A_3 = parent(abraham, isaac), P = isaac, B_3 = } P = {P = isaac} הגרסה השנייה של האינטרפרטר מבוססת על רדוקציית היעדים, עם בקרה מפורשת של סדר בחירת היעדים, באמצעות מחסנית. האינטרפרטר עובד בשני שלבים: 1. פעולת :preprocessing התוכנית הנתונה P )כללים ועובדות( מתורגמת לתוכנית חדשה 'P, עם פרדיקט יחיד BodyList)/2,rule(Head, הכוללת עובדות בלבד. 2. שאילתות מבוצעות על התוכנית החדשה באמצעות פרוצדורת solve הבאה: % Signature: solve(goal)/1 % Purpose: Goal is if it is when posed to the original program P. solve(goal) :- solve(goal, [ ]). -11-
% Signature: solve(goal, Rest_of_goals)/2 solve([ ], [ ]). solve([ ], [G Goals]) :- solve(g, Goals). solve([a B], Goals) :- append(b, Goals, Goals1), solve(a, Goals1). solve(a, Goals) :- rule(a, B), solve(b, Goals). % Signature: rule (Head, BodyList)/2 rule(member(x, [X _]), [ ]). rule(member(x, [_ Ys]), [member(x, Ys)]).?- solve(member(x, [a, b])). דוגמה 01. התוכנית עץ הוכחה חלקי: {Goal_1 = member(x, [a, b, c])} {A_2 = member(x, [a, b, c]), Goals_1 = [ ]} Rule 4 rule(member(x, [a, b, c], B_2), solve(b_2, [ ]) { X = a, X_3 = a, B_2 = [ ] } {X=a} solve([ ], [ ]) solve(member(x, [a, b, c])) solve(member(x, [a, b, c]), [ ]) {X_3 = X, Ys_3 = [b, c], B_2 = [member(x, [b, c])]} Rule 2 rule solve([member(x, [b, c])], [ ]) {A_4 = member(x, [b, c]), B_4 = [ ], Goals_4=[ ] } Rule 3 append([ ], [ ], Goals1_4), solve(member(x, [b, c]), Goals1_4). {Goals1_4=[ ]} Rule of append solve(member(x, [b, c]), [ ]). {A_5=member(X,[b, c]), Goals_5=[ ]} Rule 4 rule(member(x, [b, c]), B_5), solve(b_5, [ ]) solve([ ], [ ]) {X = b, X_6 = b, B_5 = [ ]} {X=b} -11-