
ЛК.10 – Робота зі списками в мові логічного програмування
Анотація
Поняття списку. Рекурсивна сутність списку. Обробка списків. Використання одного й того ж предикату для вирішення різних задач. Знаходження відразу всіх розв’язків. Складні списки.
10.1 Поняття списку.
Списки - це об’єкти, які можуть зберігати довільне число однотипних елементів. Наприклад:
[1,2,3], [dog, cut, canary]
Як ми вже зазначали, списки описуються за допомогою спеціального знаку (*) «зірочка». Він добавляється до кінця попередньо визначеної області.
Наприклад:
domains
integerlist = integer*
Eлemeнтом списку можуть бути любі дані, навіть інші списки. Всі елементи списку повинні належати до однієї області. Декларація областей для елементів повинна мати таку форму:
domains
elementlist = elements*
elements = integer
Складні списки є списками, які містять елементи різного типу. Враховуючи, що Пролог вимагає щоб всі елементи в списку належали одній області, ми повинні для створення складного списку використовувати функтори. Область може містити більш ніж один тип даних в якості аргументів до функторів.
Наприклад:
domains
elementlist = elements*
elements = i(integer). r(real). s(symbol)
10.2 Рекурсивна сутність списку.
Список - це рекурсивна структура, яка складається з голови та хвоста. Головою називають перший елемент списку, а хвіст - це всі інші елементи. Хвіст списку завжди буде списком, а голова - один елемент. Зазначимо, що пустий список не можна розбити на голову і хвіст.
Концептуально списки мають структуру дерева. Наприклад, список [a, b, c, d] можна представити в наступному вигляді:
list
/
a list
/
b list
/
c list
/
d []
А одноелементний список, [a] має вигляд:
list
/
a []
10.3 Обробка списків.
10.3.1 Друк списків.
Список можна роздрукувати так:
domains
list = integer* /* або ж інший тип, який ви бажаєте використовувати */
predicates
write_a_list(list)
clauses
write_a_list([]). /* якщо список пустий, тоді нічого не потрібно робити
write_a_list([H|T]) :- /* Виділяємо голову списку Н і хвіст Т,
потім роздруковуєм Н і рекурсивно обробляємо Т */
write(H), nl,
write_a_list(T).
Якщо ми задамо запит типу:
goal : write_a_list([1, 2, 3]).
Тоді список [1,2,3] буде розбито на голову Н=1 і хвіст Т = [2,3]. Предикатом write(H) роздрукується голова і рекурсивно буде оброблятись предикатом write_a_list(T) хвіст [2,3]. Процес закінчиться, коли хвіст стане пустим.
10.3.2.Підрахунок кількості елементів.
Зараз ми розглянемо як можна визначити кількість елементів у списку, іншими словами, підрахуємо довжину списку. Можна дати наступне рекурсивне визначення довжини списку :
Довжина пустого списку [ ] рівна 0.
Довжина непустого списку рівна 1 плюс довжина його хвосту.
В Пролозі цей алгоритм можна реалізувати за допомогою двох фраз:
domains
list = integer*
predicates
length_of(list, integer)
clauses
length_of([], 0).
length_of([_|T], L) :- length_of(T, TailLength),
L = TailLength + 1.
Поглянемо спочатку на другу фразу. Так, length_of [_ ,|T] порівнює любий непустий список, прив’язуючи T до хвосту списка. Значення голови не суттєво, ЕОМ буде її рахувати як один елемент.
Так ціль
goal: length_of([1,2,3], L) ,
Використовуючи другу фразу, зв’яже з T значення [2,3]. Наступний крок - обчислити довжину T. Коли це зроблено, не важливо як, тоді TailLength отримає значення 2, і ЭВМ потім зможе добавити до неї 1 та прив’язати L до 3.
Для того, щоб визначити довжину [2,3] рекурсивно викличеться length_of([2,3], TailLength. Ця ціль зрівнює другу фразу, прив’язуючи - [3] до T .
Тепер виникає задача, як знайти довжину [3], яка дорівнює 1, а потім добавить 1, щоб отримати довжину [2,3], яка буде 2. І так далі і тому подібно.
Тому length_of знов викличе себе рекурсивно, щоб отримати довжину списку [3]. Хвіст для [3] рівний [ ], тому T зв.яжеться з [], і задача зараз полягає в тому , щоб знайти довжину [], потім додати до неї 1, і знайти довжину [3]. На цей раз це зробити легко. Ціль length_of([], Tail Length зрівнюючи першу фразу, зв.яже TailLength з 0. Тепер ЭОМ може додати 1 до неї, отримуючи довжину списку [3] і повернутися до фрази виклику. Яка , в свою чергу , прибавить знову 1 , отримуючи довжину [2,3] і повернеться до фрази, яка викликала її. ця початкова фраза прибавить знову 1 , знаходячи довжину списку [1,2,3].
Ми використали тут різні змінні, щоб підкреслити той факт, що змінні з одинаковими іменами в різних фразах , або різних викликах однієї і тієї ж фрази, будуть різними.
length-of([1,2,3],L1)
length-of([2,3],L2)
length-of([3],L3)
length-of([],0)
L3 = 0 + 1 = 1
L2 = L3+ 1 = 2
L1 = L2+ 1 = 3
10.3.3.Іще один варіант підрахунку довжини списку.
Як ви бачите, предикат length_of не є хвостовою рекурсією. Чи можливо створити рекурсивно-хвостовий предикат визначення довжини списку? Можливо, але для цього потрібно прикласти певні зусилля.
Проблема з length_ of полягає у тім, що ми не можемо обчислити довжину списку, поки не підрахуємо довжину хвоста. Та все ж така можливість мається. Використаємо предикат з трьома аргументами.
- перший є список, котрий ЕОМ буде читати при кожному виклику, поки він не стане пустим.
- другий є вільним аргументом, який в кінці роботи буде визначати результат.
- третій є лічильником, котрий починається з 0 і збільшується з кожним викликом. В кінці лічильник можна уніфікувати з незв.язаним результатом.
Наступна програма реалізує наш підхід.
domains
list = integer*
predicates
length_of(list, integer, integer)
clauses
/* * * * * * * * * * * * * * * * * * * * * * *
* Якщо список пустий, тоді уніфікувати другий *
* результуючий аргумент предикату з третім *
* аргументом - лічильником *
* * * * * * * * * * * * * * * * * * * * * * */
length_of([], Result, Result).
/* * * * * * * * * * * * * * * * * * * * * * * * * * *
* В іншому випадку, додати 1 до лічильника і рекурсивно *
* зв.язати результат цього кроку з результатом в майбутньому. *
* * * * * * * * * * * * * * * * * * * * * * * * * * */
length_of([_|T], Result, Counter):- NewCounter=Counter + 1,
length_of(T, Result, NewCounter).
Запит до цієї програми повинен також мати вмонтований предикат друку. Наприклад:
goal : length_of([1,2,3], L, 0), /* починає з значенням Counter = 0 */
write(L), nl.
Остання версія програми length_of більш складна і менш логічна.
10.3.4.Модифікація списку.
Модифікація списку полягає в якійсь зміні уже існуючого списку. Традиційно, така зміна проводиться послідовною обробкою елементів списку. Прикладом модифікації списку може служити програма, яка додає до кожного елементу списку 1.
Обчислення, які повинна виконувати програма, можна описати наступним чином:
1.Зв’язати голову і хвіст початкового списку відповідно з Head і Tail.
2.Зв’язати голову і хвіст результату з Head1 і Tail1
(Head1 і Tail1 ще не мають значень).
3.Додаючи 1 до Head, отримаємо Head1.
4.Рекурсивно додайте 1 до всіх элементів Tail, отримаєте Tail1.
Наступна програма реалізує запропонований алгоритм.
domains
list = integer*
predicates
add1(list, list)
clauses
add1([], []).
add1([Head|Tail], [Head1|Tail1]) :-
Head1= Head+1,
add1(Tail,Tail1).
Розглянемо ще одну програму модифікації списку. Вона сканує вхідний список і переписує його без від.ємних чисел.
domains
list = integer*
predicates
discard_negatives(list, list)
clauses
discard_negatives([], []).
discard_negatives([H|T], ProcessedTail) :-
/* якщо Н є від.ємним, тоді пропустити його */
H < 0, !,
discard_negatives(T, ProcessedTail).
discard_negatives([H|T], [H|ProcessedTail]) :-
discard_negatives(T, ProcessedTail).
Наступний предикат копіює елементи списку, повторюючи їх два рази.
doubletail([],[]).
dou bletail([H|T],[H|H| doubletail ]) : -
doubletail(T, doubletail ).