Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Pol_Grem_-_ANSI_Common_Lisp_High_tech_-_2012.pdf
Скачиваний:
28
Добавлен:
12.03.2016
Размер:
4.85 Mб
Скачать

15

Пример: логический вывод

Вследую­щих­ трех главах­ ваше­му­ внима­нию­ предла­га­ют­ся­ три само­­ стоятель­ные­ програм­мы­. Они были­ выбра­ны,­ чтобы­ пока­зать,­ какой­ вид могут­ прини­мать­ програм­мы­ на Лиспе,­ и проде­мон­ст­ри­ро­вать­ зада­­ чи, для реше­ния­ кото­рых­ этот язык особен­но­ хоро­шо­ подхо­дит­.

Вэтой главе­ мы напи­шем­ програм­му,­ совер­шаю­щую­ логи­че­ские­ «умо­ заклю­че­ния»,­ осно­ван­ные­ на набо­ре­ правил­ «если­-то». Это класси­че­­ ский пример,­ кото­рый­ не только­ часто­ встреча­ет­ся­ в учебни­ках,­ но и от­ ража­ет­ изна­чаль­ную­ концеп­цию­ Лиспа­ как языка­ «символь­ных­ вы­ числе­ний»­. Многие­ ранние­ програм­мы­ на Лиспе­ имеют­ чер­ты той, кото­­ рая описа­на­ ниже­.

15.1. Цель

В данной­ програм­ме­ мы соби­ра­ем­ся­ представ­лять­ инфор­ма­цию­ в зна­ комой­ нам форме:­ списком­ из преди­ка­та­ и его аргу­мен­тов­. Предста­вим­ факт отцов­ст­ва­ Дональ­­да по отно­ше­нию­ к Нэнси­ таким­ обра­зом:­

(parent donald nancy)

Поми­мо­ фактов­ наша­ програм­ма­ будет­ исполь­зо­вать­ прави­ла,­ кото­рые­ сооб­ща­ют,­ что может­ быть вы­веде­но­ из имеющих­ся­ фактов­. Мы будем­ представ­лять­ эти прави­ла­ так:

(<- заго­ло­вок­ тело­)

где заго­ло­вок­ соот­вет­ст­ву­ет­ следст­вию,­ а тело ­– усло­вию­. Пере­мен­ные­ внутри­ них мы будем­ представ­лять­ симво­ла­ми,­ начи­наю­щи­ми­ся­ со зна­ ка вопро­са­. Таким­ обра­зом,­ следую­щее­ прави­ло:­

(<- (child ?x ?y) (parent ?y ?x))

254 Глава 15. Пример: логический вывод

сооб­ща­ет,­ что если­ y явля­ет­ся­ роди­те­лем­ x, то x явля­ет­ся­ ребен­ком­ y, или, гово­ря­ более­ точно,­ мы можем­ дока­зать­ любой­ факт вида­ (child x y) через­ дока­за­тель­ст­во­ (parent y x).

Тело­ (услов­ная­ часть прави­ла)­ может­ быть состав­ным­ выра­же­ни­ем,­ включаю­щим­ логи­че­ские­ опера­то­ры­ and, or, not. Так, прави­ло,­ соглас­но­ кото­ро­му,­ если­ x явля­ет­ся­ мужчи­ной­ и роди­те­лем­ y, то x – отец y, выгля­­ дит следую­щим­ обра­зом:­

(<- (father ?x ?y) (and (parent ?x ?y) (male ?x)))

Одни­ прави­ла­ могут­ опирать­ся­ на другие­. К приме­ру,­ прави­ло,­ опре­де­­ ляющее,­ явля­ет­ся­ ли x доче­рью­ y, опира­ет­ся­ на уже опре­де­лен­­ное пра­ вило­ роди­тель­ст­ва:­

(<- (daughter ?x ?y) (and (child ?x ?y) (female ?x)))

При дока­за­тель­ст­ве­ выра­же­ний­ может­ приме­нять­ся­ любое­ коли­че­ст­во­ правил,­ необ­хо­ди­мых­ для дости­же­ния­ твердой­ почвы­ фактов­. Этот про­ цесс иногда­ назы­­вают­ обрат­ной­ цепоч­кой­ логи­че­ско­го­ выво­да­ (backward chaining). Обрат­ной­ пото­му,­ что вывод­ сперва­ рассмат­ри­ва­ет­ часть-след­ ствие,­ чтобы­ увидеть,­ будет­ ли прави­ло­ полез­ным,­ преж­де­ чем продол­­ жать дока­за­тель­ст­во­ части­-усло­вия­. А цепоч­кой­ он назы­­вает­ся­ пото­му,­ что прави­ла­ зави­сят­ друг от друга,­ форми­руя­ цепоч­ку­ (скорее­ даже­ де­ рево)­ правил,­ кото­рая­ ведет­ от того,­ что мы хотим­ дока­зать,­ к тому,­ что мы уже знаем­.°

15.2. Сопоставление

Чтобы напи­сать­ нашу­ програм­му­ для обрат­ной­ цепоч­ки­ логи­че­ско­го­ выво­да,­ нам пона­до­бит­ся­ функция,­ выпол­няю­щая­ сопос­тав­ле­ние­ с об­ разцом­ (pattern-matching). Эта функция­ должна­ сравни­вать­ два списка,­ возмож­но,­ содер­жа­щие­ пере­мен­ные,­ и прове­рять,­ есть ли такая­ комби­­ нация­ значе­ний­ пере­мен­ных,­ при кото­рой­ списки­ стали­ бы равны­ми­. Напри­мер,­ если­ ?x и ?y – пере­мен­ные,­ то два списка:­

(p ?x ?y c ?x) (p a b c a)

сопос­тав­ля­ют­ся,­ если­ ?x = a и ?y = b, а списки­

(p ?x b ?y a) (p ?y b c a)

сопос­тав­ля­ют­ся­ при ?x = ?y = c.

На рис. 15.1 пока­за­на­ функция­ match, сопос­тав­ляю­щая­ два дере­ва­. Если­ они могут­ совпа­дать,­ то она возвра­ща­ет­ ассо­циа­тив­ный­ список,­ пока­­ зы­вающий,­ каким­ обра­зом­ они совпа­да­ют:­

> (match ’(p a b c a) ’(p ?x ?y c ?x)) ((?Y . B) (?X . A))

T

15.2. Сопоставление

255

>(match ’(p ?x b ?y a) ’(p ?y b c a)) ((?Y . C) (?X . ?Y))

T

>(match ’(a b c) ’(a a a)

NIL

По мере­ поэле­мент­но­го­ сравне­ния­ match нака­п­ли­ва­ет­ присваива­ния­ пе­ ремен­ным­ значе­ний,­ кото­рые­ мы назы­­ваем­ связя­ми­ (bindings), в пере­­ менной­ binds. Если­ сопос­тав­ле­ние­ завер­ши­лось­ успеш­но,­ то match воз­ вращает­ сгене­ри­ро­ван­ные­ связи,­ в против­ном­ случае­ возвра­ща­ет­ nil. Так как не все успеш­ные­ сопос­тав­ле­ния­ гене­ри­ру­ют­ хоть какие­-то свя­ зи, match, по тому­ же принци­пу,­ что и gethash, возвра­ща­ет­ второе­ значе­­ ние, указы­­вающее­ на успеш­ность­ сопос­тав­ле­ния­.

> (match ’(p ?x) ’(p ?x)) NIL

T

(defun match (x y &optional binds) (cond

((eql x y) (values binds t))

((assoc x binds) (match (binding x binds) y binds)) ((assoc y binds) (match x (binding y binds) binds)) ((var? x) (values (cons (cons x y) binds) t))

((var? y) (values (cons (cons y x) binds) t)) (t

(when (and (consp x) (consp y)) (multiple-value-bind (b2 yes)

(match (car x) (car y) binds) (and yes (match (cdr x) (cdr y) b2)))))))

(defun var? (x) (and (symbolp x)

(eql (char (symbol-name x) 0) #\?)))

(defun binding (x binds) (let ((b (assoc x binds)))

(if b

(or (binding (cdr b) binds) (cdr b)))))

Рис. 15.1. Функции­ для сопос­тав­ле­ния­

Когда­ match возвра­ща­ет­ nil и t, как произош­ло­ выше,­ это озна­ча­ет,­ что имеет­ место­ успеш­ное­ сопос­тав­ле­ние,­ не создав­шее­ связей­ для пере­мен­­ ных. Алго­ритм­ дейст­вия­ match можно­ описать­ следую­щим­ обра­зом:­

1.Если­ x и y равны­ с точки­ зрения­ eql, то они сопос­тав­ля­ют­ся;­ иначе:­

2.Если­ x – это пере­мен­ная,­ уже ассо­ции­ро­ван­ная­ со значе­ни­ем,­ то она сопос­тав­ля­ет­ся­ с y, если­ значе­ния­ совпа­да­ют;­ иначе:­

256

Глава 15. Пример: логический вывод

3.Если­ y – это пере­мен­ная,­ уже ассо­ции­ро­ван­ная­ со значе­ни­ем,­ то она сопос­тав­ля­ет­ся­ с x, если­ значе­ния­ совпа­да­ют;­ иначе:­

4.Если­ пере­мен­ная­ x не ассо­ции­ро­ва­на­ со значе­ни­ем,­ то с ней связы­­ва­ ется­ теку­щее­ значе­ние;­ иначе:­

5.Если­ пере­мен­ная­ y не ассо­ции­ро­ва­на­ со значе­ни­ем,­ то с ней связы­­ва­ ется­ теку­щее­ значе­ние;­ иначе:­

6.Два значе­ния­ сопос­тав­ля­ют­ся,­ если­ они оба – cons-ячей­ки, их carэлемен­ты­ сопос­тав­ля­ют­ся,­ а cdr сопос­тав­ля­ют­ся­ с учетом­ полу­чен­­ ных связей­.

Вот два при­мера,­ демон­ст­ри­рую­щие­ по поряд­ку­ все шесть случа­ев:­

> (match ’(p ?v b ?x d (?z ?z)) ’(p a ?w c ?y ( e e)) ’((?v . a) (?w . b)))

((?Z . E) (?Y . D) (?X . C) (?V . A) (?W . B)) T

Чтобы­ найти­ ассо­ции­ро­ван­ное­ значе­ние­ (если­ оно имеет­ся),­ match вызы­­ вает­ binding. Эта функция­ долж­на быть рекур­сив­ной,­ так как в резуль­­ тате­ сопос­тав­ле­ния­ могут­ полу­чить­ся­ пары,­ в кото­рых­ пере­мен­ная­ свя­ зана­ со значе­ни­ем­ косвен­­но, напри­мер­ ?x может­ быть связан­ с a через­ список,­ содер­жа­щий­ (?x . ?y) и (?y . a).

> (match ’(?x a) ’(?y ?y)) ((?Y . A) (?X . ?Y))

T

Сопос­та­вив­ ?x с ?y, а ?y с a, мы видим­ косвен­­ную связь пере­мен­ной­ ?x со значе­ни­ем­.

15.3. Отвечая на запросы

Теперь,­ когда­ введе­ны­ основ­ные­ концеп­ции­ сопос­тав­ле­ния,­ мы можем­ перей­ти­ непо­сред­ст­вен­но­ к назна­че­нию­ нашей­ програм­мы:­ если­ мы име­ ем выра­же­ние,­ веро­ят­но,­ содер­жа­щее­ пере­мен­ные,­ то исхо­дя­ из имею­ щихся­ фактов­ и правил­ мы можем­ найти­ все ассо­циа­ции,­ делаю­щие­ это выра­же­ние­ истин­ным­. Напри­мер,­ если­ у нас есть только­ тот факт, что

(parent donald nancy)

и мы просим­ програм­му­ дока­зать­

(parent ?x ?y)

то она вернет­ нечто,­ похо­жее­ на

(((?x . donald) (?y . nancy)))

Это озна­ча­ет,­ что наше­ выра­же­ние­ может­ быть истин­ным­ лишь в одном­ случае:­ ?x – это donald, а ?y – nancy.

15.3. Отвечая на запросы

257

Посколь­ку­ у нас есть функция­ сопос­тав­ле­ния,­ можно­ утвер­ждать,­ что мы на правиль­ном­ пути­. Теперь­ нам нужно­ научить­ся­ опре­де­лять­ пра­ вила­. Соот­вет­ст­вую­щий­ код приве­ден­ на рис. 15.2. Прави­ла­ содер­жат­ся­ в хеш-табли­це­ *rules* в соот­вет­ст­вии­ с преди­ка­та­ми­ в заго­лов­ках­. Это вводит­ огра­ни­че­ние,­ что пере­мен­ные­ не могут­ нахо­дить­ся­ на месте­ пре­ дика­тов­. От него­ мож­но изба­вить­ся,­ если­ хранить­ такие­ прави­ла­ в от­ дельном­ списке,­ но тогда­ для дока­за­тель­ст­ва­ чего­-либо­ нам при­шлось бы сопос­тав­лять­ друг другу­ каждое­ прави­ло­ из этого­ списка­.

(defvar *rules* (make-hash-table))

(defmacro <- (con &optional ant) ‘(length (push (cons (cdr ’,con) ’,ant)

(gethash (car ’,con) *rules*))))

Рис. 15.2. Опре­де­ле­ние­ правил­

Мы будем исполь­зо­вать­ макрос­ <- для опре­де­ле­ния­ и правил,­ и фактов­. Факт представ­ля­ет­ся­ в виде­ прави­ла,­ содер­жа­ще­го­ только­ заго­ло­вок­. Это соот­вет­ст­ву­ет­ наше­му­ представ­ле­нию­ о прави­лах­. Прави­ло­ гласит,­ что для дока­за­тель­ст­ва­ заго­лов­ка­ необ­хо­ди­мо­ дока­зать­ тело,­ а раз тела­ нет, то и дока­зы­­вать нече­го­. Вот два уже знако­мых­ нам приме­ра:­

>(<- (parent donald nancy))

1

>(<- (child ?x ?y) (parent ?y ?x))

1

Вызов­ <- возвра­ща­ет­ номер­ ново­го­ прави­ла­ для задан­но­го­ преди­ка­та;­ обора­чи­ва­ние­ length вокруг­ push позво­ля­ет­ избе­жать­ большо­го­ объема­ инфор­ма­ции,­ выво­ди­мой­ в toplevel.

На рис. 15.3 содер­жит­ся­ большая­ часть кода,­ нужно­го­ нам для вы­вода­.

Функция­ prove явля­ет­ся­ осью, вокруг­ кото­рой­ враща­ет­ся­ весь логи­че­­ ский вывод­. Она прини­ма­ет­ выра­же­ние­ и необя­за­тель­ный­ набор­ связей­. Если­ выра­же­ние­ не содер­жит­ логи­че­ских­ опера­то­ров,­ то вызы­ва­ет­ся­ prove-simple. Именно­ здесь проис­хо­дит­ сам обрат­ный­ вывод­. Эта функ­ ция ищет все прави­ла­ с истин­ным­ преди­ка­том­ и пыта­ет­ся­ сопос­та­вить­ заго­ло­вок­ каждо­го­ из них с фактом,­ кото­рый­ мы хотим­ дока­зать­. Затем­ для каждо­го­ совпав­ше­го­ заго­лов­ка­ дока­зы­­вает­ся­ его тело­ с учетом­ но­ вых связей,­ создан­ных­ match. Вызо­вы­ prove возвра­ща­ют­ списки­ связей,­ кото­рые­ затем­ соби­ра­ют­ся­ mapcan:

>(prove-simple ’parent ’(donald nancy) nil) (NIL)

>(prove-simple ’child ’(?x ?y) nil)

(((#:?6 . NANCY) (#:?5 . DONALD) (?Y . #:?5) (?X . #:?6)))

258 Глава 15. Пример: логический вывод

(defun prove (expr &optional binds)

(case (car expr)

(and (prove-and (reverse (cdr expr)) binds))

(or

(prove-or (cdr expr) binds))

(not (prove-not (cadr expr) binds))

(t

(prove-simple (car expr) (cdr expr) binds))))

(defun prove-simple (pred args binds)

(mapcan #’(lambda (r)

 

(multiple-value-bind (b2 yes)

 

(match args (car r) binds)

 

(when yes

 

(if (cdr r)

 

(prove (cdr r) b2)

 

(list b2)))))

 

(mapcar #’change-vars

 

(gethash pred *rules*))))

(defun change-vars (r)

(sublis (mapcar #’(lambda (v) (cons v (gensym "?")))

 

(vars-in r))

 

r))

(defun vars-in (expr)

(if (atom expr)

(if (var? expr) (list expr))

(union (vars-in (car expr))

 

(vars-in (cdr expr)))))

 

Рис. 15.3. Вывод­

Оба полу­чен­­ных значе­ния­ подтвер­жда­ют,­ что есть лишь один путь до­ каза­тель­ст­ва­. (Если­ дока­зать­ утвер­жде­ние­ не полу­чи­лось,­ возвра­ща­ет­­ ся nil.) Первый­ пример­ был вы­полнен­ без созда­ния­ каких­-либо­ связей,­ в то время­ как во втором­ приме­ре­ пере­мен­ные­ ?x и ?y были­ связа­ны­ (кос­ вен­но) с nancy и donald.

Между­ прочим,­ мы видим­ здесь хоро­ший­ пример­ выска­зан­ной­ ранее­ (стр. 39) идеи: посколь­ку­ наша­ програм­ма­ напи­са­на­ в функцио­наль­ном­ стиле,­ мы можем­ тести­ро­вать­ каждую­ функцию­ отдель­но­.

Теперь­ пара­ слов о том, зачем­ ну­жен gensym. Раз мы соби­ра­ем­ся­ исполь­­ зовать­ прави­ла,­ содер­жа­щие­ пере­мен­ные,­ то нам нужно­ избе­гать­ нали­­ чия двух правил­ с одной­ и той же пере­мен­ной­. Рассмот­рим­ два прави­ла:­

(<- (child ?x ?y) (parent ?y ?x))

(<- (daughter ?y ?x) (and (child ?y ?x) (female ?y)))

В них утвер­жда­ется,­ что для любо­го­ x и y 1) x явля­ет­ся­ ребен­ком­ y, если­ y явля­ет­ся­ роди­те­лем­ x; 2) y явля­ет­ся­ дочкой­ x, если­ y явля­ет­ся­ ребен­­ ком x и y – женщи­на­. Связь между­ пере­мен­ны­ми­ в рамках­ одно­го­ пра­

15.3. Отвечая на запросы

259

вила­ имеет­ большое­ значе­ние,­ а вот факт, что два прави­ла­ исполь­­зуют­ одина­ко­вые­ имена­ для опре­де­ле­ния­ пере­мен­ных,­ совер­шен­но­ случа­ен­.

Если­ мы восполь­зу­ем­ся­ этими­ прави­ла­ми­ в том виде,­ в каком­ только­ что их запи­са­ли,­ то они не будут­ рабо­тать­. Если­ мы попы­та­ем­ся­ дока­­ зать, что a – дочь b, то сопос­тав­ле­ние­ с заго­лов­ком­ второ­го­ прави­ла­ даст связи­ ?y = a и ?x = b. Имея такие­ связи,­ мы не сможем­ восполь­зо­вать­ся­ первым­ прави­лом:­

> (match ’(child ?y ?x) ’(child ?x ?y)

’((?y . a) (?x . b)))

NIL

Для гаран­тии­ того,­ что пере­мен­ные­ будут­ дейст­во­вать­ лишь в преде­лах­ одно­го­ прави­ла,­ мы заме­ня­ем­ все пере­мен­ные­ внутри­ прави­ла­ на gensym. Для этой цели­ опре­де­ле­на­ функция­ change-vars. Так мы приоб­ре­та­ем­ за­ щиту­ от совпа­де­ний­ с други­ми­ пере­мен­ны­ми­ в опре­де­ле­ни­ях­ других­ правил­. Но, посколь­ку­ прави­ла­ могут­ приме­нять­ся­ рекур­сив­но,­ нам также­ нужна­ защи­та­ при сопос­тав­ле­нии­ с этим же прави­лом­. Для этого­ change-vars долж­на вызы­­ваться­ не только­ при опре­де­ле­нии­ правил,­ но и при каждом­ их исполь­зо­ва­нии­.

Теперь­ оста­ет­ся­ лишь опре­де­лить­ функции­ для дока­за­тель­ст­ва­ состав­­ ных утвер­жде­ний­. Соот­вет­ст­вую­щий­ код приве­ден­ на рис. 15.4. Обра­­ ботка­ выра­же­ний­ or или not предель­но­ проста­. В первом­ случае­ мы со­ бира­ем­ все связи,­ полу­чен­­ные из каждо­го­ выра­же­ния­ в or. Во втором­ случае­ мы просто­ возвра­ща­ем­ имеющие­ся­ связи,­ если­ выра­же­ние­ внут­ ри not возвра­ти­ло­ nil.

(defun prove-and (clauses binds) (if (null clauses)

(list binds)

(mapcan #’(lambda (b)

(prove (car clauses) b)) (prove-and (cdr clauses) binds))))

(defun prove-or (clauses binds)

(mapcan #’(lambda (c) (prove c binds)) clauses))

(defun prove-not (clause binds) (unless (prove clause binds)

(list binds)))

Рис. 15.4. Логи­че­ские­ опера­то­ры­

Функция­ prove-and лишь чуть сложнее­. Она рабо­та­ет­ как фильтр, дока­­ зы­вая первое­ выра­же­ние­ для каждо­го­ из набо­ров­ связей,­ полу­чен­­ных из осталь­ных­ выра­же­ний­. По этой причи­не­ выра­же­ния­ внутри­ and рас­

260

Глава 15. Пример: логический вывод

сматри­ва­ют­ся­ в обрат­ном­ поряд­ке­. Пере­во­ра­чи­ва­ние­ резуль­та­та­ proveand компен­си­ру­ет­ это явле­ние­ (так как резуль­ти­рую­щий­ список­ выра­­ жений­ форми­ру­ет­ся­ функци­ей­ в обрат­ном­ поряд­ке,­ в конце­ ее выпол­не­­ ния этот список­ нужно­ развер­нуть)­ .

Теперь­ у нас есть рабо­чая­ програм­ма,­ но она не очень удобна­ для конеч­­ ного­ пользо­ва­те­ля­. Разби­рать­ списки­ связей,­ возвра­щае­мых­ prove, до­ вольно­ сложно,­ а ведь с услож­не­ни­ем­ выра­же­ний­ они будут­ только­ рас­ ти. Эту пробле­му­ реша­ет­ макрос­ with-answer, изобра­жен­ный­ на рис. 15.5. Он при­нима­ет­ запрос­ (не вычис­лен­­ный) и вычис­ля­ет­ свое тело­ для каж­ дого­ набо­ра­ связей,­ полу­чен­­ных при обра­бот­ке­ запро­са,­ связы­­вая каж­ дую пере­мен­ную­ запро­са­ со значе­ни­ем,­ кото­рое­ она имеет­ в теку­щем­ экзем­п­ля­ре­ связей:­

> (with-answer (parent ?x ?y)

(format t "~A is the parent of ~A.~%" ?x ?y)) DONALD is the parent of NANCY.

NIL

Этот макрос­ расшиф­ро­вы­ва­ет­ полу­чен­­ные связи­ и предос­тав­ля­ет­ удоб­ ный способ­ исполь­зо­ва­ния­ prove в наших­ програм­мах­. На рис. 15.6 по­ казан­ резуль­тат­ его раскры­тия,­ а рис. 15.7 демон­ст­ри­ру­ет­ неко­то­рые­ приме­ры­ исполь­зо­ва­ния­ этого­ макро­са­.

(defmacro with-answer (query &body body) (let ((binds (gensym)))

‘(dolist (,binds (prove ’,query)) (let ,(mapcar #’(lambda (v)

‘(,v (binding ’,v ,binds))) (vars-in query))

,@body))))

Рис. 15.5. Интер­фейс­ный­ макрос­

(with-answer (p ?x ?y) (f ?x ?y))

;;; раскры­ва­ет­ся­ в:

(dolist (#:g1 (prove ’(p ?x ?y))) (let ((?x (binding ’?x #:g1))

(?y (binding ’?y #:g1))) (f ?x ?y)))

Рис. 15.6. Раскры­тие­ вызо­ва­ with-answer

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]