Программирование на языке ПРОЛОГ для искуственного интеллекта

       

Bagof , setof и findall


При помощи механизма автоматического перебора можно получить одни за другим все объекты, удовлетворяющие некоторой цели. Всякий раз, как порождается новое решение, предыдущее пропадает и становится с этого момента недоступным. Однако у нас может возникнуть желание получить доступ ко всем порожденным объектам сразу, например собрав их в список. Встроенные предикаты bagof (набор) и setof (множество) обеспечивают такую возможность; вместо них иногда используют предикат findall (найти все).

Цель

        bagof( X, P, L)

порождает список L всех объектов X, удовлетворяющих цели Р. Обычно bagof имеет смысл применять только тогда, когда Х и Р содержат общие переменные. Например, допустим, что мы включили в программу следующую группу предложений для разбиения букв (из некоторого множества) на два класса - гласные и согласные:

        класс( а, глас).
        класс( b, согл).
        класс( с, согл).
        класс( d, согл).
        класс( е, глас).
        класс( f, согл).

Тогда мы можем получить список всех согласных, упомянутых в этих предложениях, при помощи цели:

        ?-  bagof( Буква, класс( Буква, согл), Буквы).

        Буквы = [d, c, d, f]

Если же мы в указанной цели оставим класс букв неопределенным, то, используя автоматический перебор, получим два списка букв, каждый из которых соответствует одному из классов:

        ?-  bagof( Буква, класс( Буква, Класс), Буквы).



        Класс = глас
        Буквы = [а,е]

        Класс = согл
        Буквы = [b, c, d, f]

Если bagof( X, Р, L) не находит ни одного решения для Р, то цель bagof просто терпит неуспех.
Если один и тот же Х найден многократно, то все его экземпляры будут занесены в L, что приведет к появлению в L повторяющихся элементов.

Предикат setof работает аналогично предикату bagof. Цель

        setof( X, P, L)

как и раньше, порождает список  L  объектов   X,  удовлетворяющих  Р.  Только на этот раз список  L  будет упорядочен, а из всех повторяющихся элементов, если таковые есть, в него попадет только один. Упорядочение происходит по алфавиту или по отношению '<', если элементы списка - числа. Если элементы списка - структуры, то они упорядочиваются по своим главным функторам. Если же главные функторы совпадают, то решение о порядке таких термов принимается по их первым несовпадающим функторам, расположенным выше и левее других (по дереву). На вид объектов, собираемых в список, ограничения нет. Поэтому можно, например, составить список пар вида

        Класс / Буква

при этом гласные будут расположены в списке первыми ("глас" по алфавиту раньше "согл"):

        ?-  setof( Класс/Буква, класс( Буква, Класс), Спис).

        Спис = [глас/а, глас/е, согл/b, согл/с, согл/d, согл/f]

Еще одним предикатом этого семейства, аналогичным bagof, является findall.

        findall( X, P, L)

тоже порождает список объектов, удовлетворяющих Р. Он отличается от bagof тем, что собирает в список все объекты X, не обращая внимание на (возможно) отличающиеся для них конкретизации тех переменных из P, которых нет в X. Это различие видно из следующего примера:

        ?-  findall( Буква, класс( Буква, Класс), Буквы).

        Буквы= [a, b, c, d, e, f]

Если не существует ни одного объекта X, удовлетворяющего P, то findall все равно имеет успех и выдает L = [ ].



Если в используемой реализации Пролога отсутствует встроенный предикат findall, то его легко запрограммировать следующим образом. Все решения для Р порождаются искусственно вызываемыми возвратами. Каждое решение, как только оно получено, немедленно добавляется к базе данных, чтобы не потерять его после нахождения следующего решения. После того, как будут получены и сохранены все решения, их нужно собрать в список, а затем удалить из базы данных при помощи retract. Весь процесс можно представлять себе как построение очереди из порождаемых решений. Каждое вновь порождаемое решение добавляется в конец этой очереди при помощи assert. Когда все решения собраны, очередь расформировывается. Заметим также, что конец очереди надо пометить, например, атомом "дно" (который, конечно, должен отличаться от любого ожидаемого решения). Реализация findall в соответствии с описанным методом показана на рис. 7.4.

line(); findall( X, Цель, ХСпис) :-
    саll( Цель),                                 % Найти решение
    assert( очередь( X) ),                % Добавить егo
    fail;                 % Попытаться найти еще решения
    assertz( очередь( дно) ),
                         % Пометить конец решений
    собрать( ХСпис).                     % Собрать решения в список

собрать( L) :-
     retract( очередь(Х) ),  !,

                         % Удалить следующее решение
    ( Х == дно,  !,  L = [ ];
                         % Конец решений?
    L = [X | Остальные], собрать( Остальные) ).
                         % Иначе собрать остальные

line(); Рис. 7. 4.  Реализация отношения findall.


Содержание раздела