Основы программирования на языке Visual Prolog

Списки. Полиморфизм

Разбить на страницы
Показывать лекцию целиком

Списки в языке Пролог являются одной из основных структур данных. Рекурсивная природа списков предполагает их рекурсивную обработку. В данной главе рассматриваются понятия полиморфизма и параметрического полиморфизма. Определяются предикаты обработки списка — полиморфные предикаты вычисления длины списка, определения принадлежности элемента списку, соединения списков, замены элементов списка, обращения списка, удаления элементов из списка, а также недетерминированные предикаты поиска подсписков списка, его префиксов и суффиксов, и предикат возвращения произвольного элемента списка и его остатка либо вставки элемента в произвольное место списка.

6.1. Параметрический полиморфизм

Если природа элементов списка не важна, как, например, при вычислении длины списка, то для объявления предиката обработки списков обычно используются полиморфные домены. Рекурсивно длина списка определяется как сумма длины хвоста списка и единицы, длина пустого списка равна нулю (см. ниже).

Имя полиморфного домена в объявлении предиката пишется с прописной буквы в виде:

class predicates
    length: (Elem*) -> positive.

Имя Elem можно заменить любым другим именем, начинающимся с прописной буквы, например:

class predicates
    length: (A*) -> positive.

Переменная (Elem или A) обозначает произвольный домен элементов списка. Предикаты, аргументы которых могут принадлежать произвольным доменам, называются полиморфными предикатами. Определение пролиморфного предиката не зависит от природы его аргументов. Например, предикат вычисления длины списка одинаково определяется для списка чисел, списка строк и т. д. Но предикат, который вычисляет сумму элементов списка, не может быть полиморфным, так как в его определении участвует операция сложения чисел, которая не применима, например, к строкам (напомним, что все элементы списка должны принадлежать одному и тому же домену). Полиморфизм, в котором предикаты определяются независимо от типа аргументов, называется параметрическим.

6.2. Параметры в именах доменов

Имена доменов могут иметь аргументы — параметры, которые представляют также имена доменов. В качестве аргументов имени домена в объявлении этого домена могут указываться только переменные. Аргументы имен доменов заключаются в фигурные скобки, они являются попарно независимыми. Например, объявление предикатного домена и принадлежащих ему предикатов может иметь вид:

domains
    op{A, B} = (A*) -> B.

class predicates
    length : op{A, positive}.    % длина списка
    sum : op{integer, integer}.    % сумма элементов списка
clauses
    length([]) = 0.
    length([_ | T]) = 1 + length(T).

    sum([]) = 0.
    sum([H | T]) = H + sum(T).

Функции вычисления длины списка и суммы его элементов определены выше без применения хвостовой рекурсии. Поэтому их нельзя использовать для списков, содержащих сотни тысяч или миллионы элементов. Эффективное определение этих операций, с использованием счетчика, позволяет сделать это (листинг 6.1). В программе используется хвостовая рекурсия.

class predicates
    length: (A*) -> positive.
    length: (A*, positive) -> positive.
clauses
    length(L) = length(L, 0).

    length([], N) = N.
    length([_ | T], N) = length(T, N + 1).

    run():-
        write(length([1, 2, 3]) + length(["A", "B"])),
        _ = readLine().

Упражнение 1. Определите предикат, вычисляющий сумму четных элементов списка с помощью хвостовой рекурсии.

В следующей программе определяются операции проверки принадлежности элемента списку, а также возвращения произвольного элемента списка, в предикатном и в функциональном стиле. Операция определяется рекурсивно, в соответствии с правилом — элемент принадлежит списку, если он совпадает с головой списка или принадлежит его хвосту.

class predicates
    member_dt: (A, A*) determ.
    member: (A [out], A*) nondeterm.
    member_nd: (A*) -> A nondeterm.
clauses
    member_dt(H, [H | _]):- !.   % проверка принадлежности
    member_dt(H, [_ | T]):-
        member_dt(H, T).

    member(H, [H | _]).    % возвращение элемента списка
    member(H, [_ | T]):-
        member(H, T).

    member_nd([H | _]) = H.
    member_nd([_ | T]) = member_nd(T).

    run():-
        X = 2, L = [1, 2, 3],
        if member_dt(X, L) then
            writef("Элемент % принадлежит списку %\n\n", X, L)
        else
            writef("Элемент % не принадлежит списку %\n\n", X, L)
        end if,

        writef("Элементы списка %:\n", L),
        foreach member(Y, L) do
            write(Y), nl
        end foreach,

        L1 = ["H", "E", "L", "L", "O"],
        writef("\nЭлементы списка %:\n", L1),
        write(member_nd(L1)), nl,
        fail;
        _ = readLine().

Упражнение 2. Определите предикаты, которые возвращают:

  • каждый второй элемент списка;
  • только второй элемент списка;
  • элементы списка целых чисел, делящиеся на 3 без остатка.
  • В классе list, который входит в набор Prolog Foundation Classes (PFC) основных классов языка Visual Prolog имеется предикат проверки принадлежности элемента списку isMember/2 и предикат getMember_nd/1, недетерминированно возвращающий элементы списка и определенный в виде функции. В версии 7.5 языка Visual Prolog появился оператор in, который можно использовать для выполнения обеих данных операций. Например:

    if 2 in [1, 2, 3] then write("2 принадлежит списку")
     else write("2 не принадлежит списку") end if, nl, nl,
            
     foreach X in [1, 2, 3] do write(X), nl end foreach.
    
    

    Следующая программа посвящена предикату append, определение которого позволяет использовать его большим количеством способов, в частности:

  • для соединения списков;
  • для определения префикса (начального отрезка списка), который нужно присоединить к заданному суффиксу (конечному отрезку списка), чтобы получить заданный список;
  • для определения суффикса, который нужно присоединить к заданному префиксу, чтобы получить заданный список;
  • для поиска всех возможных разбиений на префиксы и суффиксы;
  • для многого другого.
  • class predicates
        append: (A*, A*, A*) nondeterm anyflow.
        append: (A*, A*) -> A*.
    clauses
        append([], L, L).    % разнообразное использование
        append([H | L1], L2, [H | L]):-
            append(L1, L2, L).
    
        append([], L) = L.    % соединение двух списков в один
        append([H | L1], L2) = [H | append(L1, L2)].
    
        run():-
            append([1, 2], [2, 3], L0),
            write(L0), nl,
            write(append([1, 2], [2, 3])), nl,
    
            L = [1, 2, 3, 4, 5],
            append(L1, [4, 5], L),
            write(L1), nl,
            append([1, 2, 3], L2, L),
            write(L2), nl, nl,
            append(P, S, L),
                writef("% - %\n", P, S),
            fail;
    
            L = [1, 2, 3, 4, 5, 6, 7],
            append(_, [X, _, _], L),
            writef("\nТретий с конца элемент списка - %", X),
            append([_, _, Y], _, L),
            writef("\nТретий с начала элемент списка - %", Y),
            fail;
            _ = readLine().
    

    Упражнение 3. Найдите с помощью предиката append:

  • последний элемент списка;
  • префиксы списка;
  • суффиксы списка;
  • список без двух последних элементов исходного списка.
  • В классе list определены предикаты append, которые могут иметь от двух до пяти аргументов, выполняющие операцию соединения списков. Кроме этого, имеется предикат appendList/1, который соединяет список списков элементов в один список.

    В приведенной ниже программе непосредственно генерируются подсписки списка — префиксы, суффиксы и все подсписки.

    class predicates
        prefix: (A*) -> A* multi.
        suffix: (A*) -> A* multi.
        sublist: (A*) -> A* nondeterm.
    clauses
        prefix(_) = [].        % префикс списка
        prefix([H | T]) = [H | prefix(T)].
    
        suffix(L) = L.        % суффикс списка
        suffix([_| T]) = suffix(T).
    
        sublist([]) = [].        % подсписок списка
        sublist(L) = S:-
            S = prefix(L),
            S <> [].
        sublist([_ | L]) = sublist(L).
    
        run():-
            write(prefix([1, 2, 3, 4])), nl,
            fail;
            nl, write(suffix([1, 2, 3, 4])), nl,
            fail;
            nl, write(sublist([1, 2, 3, 4])), nl,
            fail;
            _ = readLine().
    

    Упражнение 4.

  • Сгенерируйте подсписки списка четной длины, не используя предикат вычисления длины списка.
  • Определите детерминированный предикат, который по двум входным спискам определяет, является ли первый из них подсписком второго списка.
  • В приведенной ниже программе реализованы операция замены одного элемента списка другим, а также операция замены каждого n-го элемента списка заданным элементом. В определении второго предиката используется счетчик.

    class predicates
        replace: (A*, A What, A With) -> A*.
        replace_nth: (A*, positive N, A With) -> A*.
        replace_nth: (A*, positive Counter, positive N, A With) -> A*.
    clauses
        replace([A | T], A, B) = [B | replace(T, A, B)]:- !.
        replace([H | T], A, B) = [H | replace(T, A, B)].
        replace([], _, _) = [].
    
        replace_nth(L, N, A) = replace_nth(L, N, N, A):-
            N > 1,
            !.
        replace_nth(L, _, _) = L.
    
        replace_nth([_ | L], 1, N, A) = [A | replace_nth(L, N, N, A)]:- !.
        replace_nth([H | L], C, N, A) = [H | replace_nth(L, C - 1, N, A)].
        replace_nth([], _, _, _) = [].
    
        run():-
            L = [math::random(10) || _ = std::fromTo(1, 20)],
            write(L), nl,
            write(replace(L, 0, 100)), nl,
            write(replace_nth(L, 6, 333)), nl,
            _ = readLine().
    

    Предикат fromTo (см. определение предиката run) недетерминированно возвращает целые числа в заданных пределах. Например, цель

    X = fromTo(1, 3)
    
    

    имеет следующие решения:

    X = 1
    X = 2
    X = 3
    

    Упражнение 5. Определите предикат, который:

  • заменяет каждый второй элемент списка заданным элементом, не используя счетчик;
  • удаляет каждый n-й элемент списка.
  • В следующей программе определена операция обращения списка, в результате выполнения которой список записывается в обратном порядке. Например, список [1, 2, 3] преобразуется в список [3, 2, 1]. В определении операции используется вспомогательный аргумент — список, в который перекладываются по одному элементы исходного списка.

    class predicates
        reverse: (A*) -> A*.
        reverse: (A*, A*) -> A*.
    clauses
        reverse(L) = reverse(L, []).
    
        reverse([], L) = L.
        reverse([A | L], L1) = reverse(L, [A | L1]).
    
        run():-
            write(reverse([1, 2, 3, 4, 5])), nl,
            write(reverse([1, 2], [3, 4, 5])),
        _ = readLine().
    

    В классе list имеется предикат reverse/1, который выполняет операцию обращения списка.

    В приведенной ниже программе список целых неотрицательных чисел разбивается на два списка. В первый список попадают все четные элементы, а во второй — все нечетные элементы.

    class predicates
        split: (unsigned*, unsigned* [out], unsigned* [out]).
    clauses
        split([A | L], [A | L1], L2):-
            A mod 2 = 0,
            !,
            split(L, L1, L2).
        split([A | L], L1, [A | L2]):-
            split(L, L1, L2).
        split([], [], []).
    
        run():-
            split([1, 2, 3, 4, 5, 9, 8, 7, 6], L1, L2),
            write(L1), nl,
            write(L2),
            _ = readLine().
    

    Следующая программа посвящена двум важным предикатам. Первый из них — это предикат delete, который удаляет первое вхождение элемента в список. Если такого элемента в списке нет, предикат принимает значение ложь. Второй предикат — это предикат select. Предикат можно использовать двояким образом. С одной стороны, он недетерминированно возвращает произвольный элемент списка и список без этого элемента. С другой стороны, он недетерминированно вставляет заданный элемент на произвольное место в заданном списке, так что в результате получается новый список.

    class predicates
        delete: (A, A*) -> A* determ.
        select: (A, A*, A*) nondeterm (o,i,o) multi (i,o,i).
    clauses
        delete(A, [A | L]) = L:- !.
        delete(A, [B | L]) = [B | delete(A, L)].
    
        select(A, [A | L], L).
        select(A, [B | L], [B | L1]):-
            select(A, L, L1).
    
        run():-
            L = delete(4, [1, 2, 3, 4, 5]),
            write(L), nl,
            fail;
            nl,
            select(A, [1, 2, 3, 4, 5], L),
                write(A, " - ", L), nl,
            fail;
            nl,
            select(100, L, [1, 2, 3, 4, 5]),
                write(L), nl,
            fail;
            _ = readLine().
    

    Как упоминалось выше, элементы списка в языке Visual Prolog должны принадлежать одному и тому же домену. Поэтому в нем нельзя использовать вложенные списки напрямую и строить термы вида [[0], 1, [2, 3, [4, 5, []]]]. Но такие списки можно смоделировать. В следующей программе рекурсивно определяется домен элементов списка, включающий как атомарные элементы, так и вложенные списки. Аргументом функтора list/1 является список элементов исходного домена. Атомы — это термы с функтором at/1. В программе реализуется операция линеаризации списка — приведения его к списку атомарных элементов. Идея определения операции линеаризации с помощью вспомогательного списка, играющего роль стека, описана в [9].

    domains
        elem{A} = at(A); list(elem{A}*).
    
    class predicates
        flatten: (elem{A}*) -> elem{A}*.
        flatten: (elem{A}*, elem{A}**) -> elem{A}*.
    clauses
        flatten(List) = flatten(List, []).
    
        flatten([list(L) | Tail], AuxList) =  flatten(L, [Tail | AuxList]):- !.
        flatten([Head | Tail], AuxList) =  [Head | flatten(Tail, AuxList)].
        flatten([], [L | AuxList]) =  flatten(L, AuxList).
        flatten([], []) = [].
    
        run():-
            L = [list([at(0)]), at(1), 
                    list([at(2), at(3), list([at(4), at(5), list([])])])],
            writef("%\n%", L, flatten(L)),
            _ = readLine().
    

    В языке Visual Prolog имеются развитые средства обработки исключений. Например, если функция определена только на непустых списках, то ее можно "доопределить" так, что она станет всюду определенной:

    class predicates
        first: (Elem*) -> Elem*.
    clauses
        first([H | _]) = [H].
        first([]) = _:-
            exception::raise_error(predicate_name(),
                "  Input list is empty.").
    

    Если аргумент предиката first равен пустому списку, то возбуждается исключение. Предикат predicate_name возвращает имя предиката, в определении которого он участвует.

    Упражнения

  • Проверьте, является ли список палиндромом.
  • Вычислите среднее арифметическое элементов списка, состоящего из целых чисел.
  • Проверьте, каждый ли элемент первого списка, содержится во втором списке.
  • Проверьте, является ли список упорядоченным по возрастанию или по убыванию.
  • Найдите позицию первого вхождения подсписка в список.
  • Вычислите все позиции заданного элемента в списке.
  • Разбейте список на отрезки длиной по n элементов. Последний подсписок может содержать меньшее число элементов.
  • Поменяйте местами два элемента списка, стоящих на заданных позициях.
  • Вычислите номер заданного атомарного элемента в списке, содержащем вложенные списки (см. листинг 6.9).
  • Страницы:

    Списки в языке Пролог являются одной из основных структур данных. Рекурсивная природа списков предполагает их рекурсивную обработку. В данной главе рассматриваются понятия полиморфизма и параметрического полиморфизма. Определяются предикаты обработки списка — полиморфные предикаты вычисления длины списка, определения принадлежности элемента списку, соединения списков, замены элементов списка, обращения списка, удаления элементов из списка, а также недетерминированные предикаты поиска подсписков списка, его префиксов и суффиксов, и предикат возвращения произвольного элемента списка и его остатка либо вставки элемента в произвольное место списка.

    6.1. Параметрический полиморфизм

    Если природа элементов списка не важна, как, например, при вычислении длины списка, то для объявления предиката обработки списков обычно используются полиморфные домены. Рекурсивно длина списка определяется как сумма длины хвоста списка и единицы, длина пустого списка равна нулю (см. ниже).

    Имя полиморфного домена в объявлении предиката пишется с прописной буквы в виде:

    class predicates
        length: (Elem*) -> positive.
    

    Имя Elem можно заменить любым другим именем, начинающимся с прописной буквы, например:

    class predicates
        length: (A*) -> positive.
    

    Переменная (Elem или A) обозначает произвольный домен элементов списка. Предикаты, аргументы которых могут принадлежать произвольным доменам, называются полиморфными предикатами. Определение пролиморфного предиката не зависит от природы его аргументов. Например, предикат вычисления длины списка одинаково определяется для списка чисел, списка строк и т. д. Но предикат, который вычисляет сумму элементов списка, не может быть полиморфным, так как в его определении участвует операция сложения чисел, которая не применима, например, к строкам (напомним, что все элементы списка должны принадлежать одному и тому же домену). Полиморфизм, в котором предикаты определяются независимо от типа аргументов, называется параметрическим.

    6.2. Параметры в именах доменов

    Имена доменов могут иметь аргументы — параметры, которые представляют также имена доменов. В качестве аргументов имени домена в объявлении этого домена могут указываться только переменные. Аргументы имен доменов заключаются в фигурные скобки, они являются попарно независимыми. Например, объявление предикатного домена и принадлежащих ему предикатов может иметь вид:

    domains
        op{A, B} = (A*) -> B.
    
    class predicates
        length : op{A, positive}.    % длина списка
        sum : op{integer, integer}.    % сумма элементов списка
    clauses
        length([]) = 0.
        length([_ | T]) = 1 + length(T).
    
        sum([]) = 0.
        sum([H | T]) = H + sum(T).
    

    Функции вычисления длины списка и суммы его элементов определены выше без применения хвостовой рекурсии. Поэтому их нельзя использовать для списков, содержащих сотни тысяч или миллионы элементов. Эффективное определение этих операций, с использованием счетчика, позволяет сделать это (листинг 6.1). В программе используется хвостовая рекурсия.

    class predicates
        length: (A*) -> positive.
        length: (A*, positive) -> positive.
    clauses
        length(L) = length(L, 0).
    
        length([], N) = N.
        length([_ | T], N) = length(T, N + 1).
    
        run():-
            write(length([1, 2, 3]) + length(["A", "B"])),
            _ = readLine().
    

    Упражнение 1. Определите предикат, вычисляющий сумму четных элементов списка с помощью хвостовой рекурсии.

    В следующей программе определяются операции проверки принадлежности элемента списку, а также возвращения произвольного элемента списка, в предикатном и в функциональном стиле. Операция определяется рекурсивно, в соответствии с правилом — элемент принадлежит списку, если он совпадает с головой списка или принадлежит его хвосту.

    class predicates
        member_dt: (A, A*) determ.
        member: (A [out], A*) nondeterm.
        member_nd: (A*) -> A nondeterm.
    clauses
        member_dt(H, [H | _]):- !.   % проверка принадлежности
        member_dt(H, [_ | T]):-
            member_dt(H, T).
    
        member(H, [H | _]).    % возвращение элемента списка
        member(H, [_ | T]):-
            member(H, T).
    
        member_nd([H | _]) = H.
        member_nd([_ | T]) = member_nd(T).
    
        run():-
            X = 2, L = [1, 2, 3],
            if member_dt(X, L) then
                writef("Элемент % принадлежит списку %\n\n", X, L)
            else
                writef("Элемент % не принадлежит списку %\n\n", X, L)
            end if,
    
            writef("Элементы списка %:\n", L),
            foreach member(Y, L) do
                write(Y), nl
            end foreach,
    
            L1 = ["H", "E", "L", "L", "O"],
            writef("\nЭлементы списка %:\n", L1),
            write(member_nd(L1)), nl,
            fail;
            _ = readLine().
    

    Упражнение 2. Определите предикаты, которые возвращают:

  • каждый второй элемент списка;
  • только второй элемент списка;
  • элементы списка целых чисел, делящиеся на 3 без остатка.
  • В классе list, который входит в набор Prolog Foundation Classes (PFC) основных классов языка Visual Prolog имеется предикат проверки принадлежности элемента списку isMember/2 и предикат getMember_nd/1, недетерминированно возвращающий элементы списка и определенный в виде функции. В версии 7.5 языка Visual Prolog появился оператор in, который можно использовать для выполнения обеих данных операций. Например:

    if 2 in [1, 2, 3] then write("2 принадлежит списку")
     else write("2 не принадлежит списку") end if, nl, nl,
            
     foreach X in [1, 2, 3] do write(X), nl end foreach.
    
    

    Следующая программа посвящена предикату append, определение которого позволяет использовать его большим количеством способов, в частности:

  • для соединения списков;
  • для определения префикса (начального отрезка списка), который нужно присоединить к заданному суффиксу (конечному отрезку списка), чтобы получить заданный список;
  • для определения суффикса, который нужно присоединить к заданному префиксу, чтобы получить заданный список;
  • для поиска всех возможных разбиений на префиксы и суффиксы;
  • для многого другого.
  • class predicates
        append: (A*, A*, A*) nondeterm anyflow.
        append: (A*, A*) -> A*.
    clauses
        append([], L, L).    % разнообразное использование
        append([H | L1], L2, [H | L]):-
            append(L1, L2, L).
    
        append([], L) = L.    % соединение двух списков в один
        append([H | L1], L2) = [H | append(L1, L2)].
    
        run():-
            append([1, 2], [2, 3], L0),
            write(L0), nl,
            write(append([1, 2], [2, 3])), nl,
    
            L = [1, 2, 3, 4, 5],
            append(L1, [4, 5], L),
            write(L1), nl,
            append([1, 2, 3], L2, L),
            write(L2), nl, nl,
            append(P, S, L),
                writef("% - %\n", P, S),
            fail;
    
            L = [1, 2, 3, 4, 5, 6, 7],
            append(_, [X, _, _], L),
            writef("\nТретий с конца элемент списка - %", X),
            append([_, _, Y], _, L),
            writef("\nТретий с начала элемент списка - %", Y),
            fail;
            _ = readLine().
    

    Упражнение 3. Найдите с помощью предиката append:

  • последний элемент списка;
  • префиксы списка;
  • суффиксы списка;
  • список без двух последних элементов исходного списка.
  • В классе list определены предикаты append, которые могут иметь от двух до пяти аргументов, выполняющие операцию соединения списков. Кроме этого, имеется предикат appendList/1, который соединяет список списков элементов в один список.

    В приведенной ниже программе непосредственно генерируются подсписки списка — префиксы, суффиксы и все подсписки.

    class predicates
        prefix: (A*) -> A* multi.
        suffix: (A*) -> A* multi.
        sublist: (A*) -> A* nondeterm.
    clauses
        prefix(_) = [].        % префикс списка
        prefix([H | T]) = [H | prefix(T)].
    
        suffix(L) = L.        % суффикс списка
        suffix([_| T]) = suffix(T).
    
        sublist([]) = [].        % подсписок списка
        sublist(L) = S:-
            S = prefix(L),
            S <> [].
        sublist([_ | L]) = sublist(L).
    
        run():-
            write(prefix([1, 2, 3, 4])), nl,
            fail;
            nl, write(suffix([1, 2, 3, 4])), nl,
            fail;
            nl, write(sublist([1, 2, 3, 4])), nl,
            fail;
            _ = readLine().
    

    Упражнение 4.

  • Сгенерируйте подсписки списка четной длины, не используя предикат вычисления длины списка.
  • Определите детерминированный предикат, который по двум входным спискам определяет, является ли первый из них подсписком второго списка.
  • В приведенной ниже программе реализованы операция замены одного элемента списка другим, а также операция замены каждого n-го элемента списка заданным элементом. В определении второго предиката используется счетчик.

    class predicates
        replace: (A*, A What, A With) -> A*.
        replace_nth: (A*, positive N, A With) -> A*.
        replace_nth: (A*, positive Counter, positive N, A With) -> A*.
    clauses
        replace([A | T], A, B) = [B | replace(T, A, B)]:- !.
        replace([H | T], A, B) = [H | replace(T, A, B)].
        replace([], _, _) = [].
    
        replace_nth(L, N, A) = replace_nth(L, N, N, A):-
            N > 1,
            !.
        replace_nth(L, _, _) = L.
    
        replace_nth([_ | L], 1, N, A) = [A | replace_nth(L, N, N, A)]:- !.
        replace_nth([H | L], C, N, A) = [H | replace_nth(L, C - 1, N, A)].
        replace_nth([], _, _, _) = [].
    
        run():-
            L = [math::random(10) || _ = std::fromTo(1, 20)],
            write(L), nl,
            write(replace(L, 0, 100)), nl,
            write(replace_nth(L, 6, 333)), nl,
            _ = readLine().
    

    Предикат fromTo (см. определение предиката run) недетерминированно возвращает целые числа в заданных пределах. Например, цель

    X = fromTo(1, 3)
    
    

    имеет следующие решения:

    X = 1
    X = 2
    X = 3
    

    Упражнение 5. Определите предикат, который:

  • заменяет каждый второй элемент списка заданным элементом, не используя счетчик;
  • удаляет каждый n-й элемент списка.
  • В следующей программе определена операция обращения списка, в результате выполнения которой список записывается в обратном порядке. Например, список [1, 2, 3] преобразуется в список [3, 2, 1]. В определении операции используется вспомогательный аргумент — список, в который перекладываются по одному элементы исходного списка.

    class predicates
        reverse: (A*) -> A*.
        reverse: (A*, A*) -> A*.
    clauses
        reverse(L) = reverse(L, []).
    
        reverse([], L) = L.
        reverse([A | L], L1) = reverse(L, [A | L1]).
    
        run():-
            write(reverse([1, 2, 3, 4, 5])), nl,
            write(reverse([1, 2], [3, 4, 5])),
        _ = readLine().
    

    В классе list имеется предикат reverse/1, который выполняет операцию обращения списка.

    В приведенной ниже программе список целых неотрицательных чисел разбивается на два списка. В первый список попадают все четные элементы, а во второй — все нечетные элементы.

    class predicates
        split: (unsigned*, unsigned* [out], unsigned* [out]).
    clauses
        split([A | L], [A | L1], L2):-
            A mod 2 = 0,
            !,
            split(L, L1, L2).
        split([A | L], L1, [A | L2]):-
            split(L, L1, L2).
        split([], [], []).
    
        run():-
            split([1, 2, 3, 4, 5, 9, 8, 7, 6], L1, L2),
            write(L1), nl,
            write(L2),
            _ = readLine().
    

    Следующая программа посвящена двум важным предикатам. Первый из них — это предикат delete, который удаляет первое вхождение элемента в список. Если такого элемента в списке нет, предикат принимает значение ложь. Второй предикат — это предикат select. Предикат можно использовать двояким образом. С одной стороны, он недетерминированно возвращает произвольный элемент списка и список без этого элемента. С другой стороны, он недетерминированно вставляет заданный элемент на произвольное место в заданном списке, так что в результате получается новый список.

    class predicates
        delete: (A, A*) -> A* determ.
        select: (A, A*, A*) nondeterm (o,i,o) multi (i,o,i).
    clauses
        delete(A, [A | L]) = L:- !.
        delete(A, [B | L]) = [B | delete(A, L)].
    
        select(A, [A | L], L).
        select(A, [B | L], [B | L1]):-
            select(A, L, L1).
    
        run():-
            L = delete(4, [1, 2, 3, 4, 5]),
            write(L), nl,
            fail;
            nl,
            select(A, [1, 2, 3, 4, 5], L),
                write(A, " - ", L), nl,
            fail;
            nl,
            select(100, L, [1, 2, 3, 4, 5]),
                write(L), nl,
            fail;
            _ = readLine().
    

    Как упоминалось выше, элементы списка в языке Visual Prolog должны принадлежать одному и тому же домену. Поэтому в нем нельзя использовать вложенные списки напрямую и строить термы вида [[0], 1, [2, 3, [4, 5, []]]]. Но такие списки можно смоделировать. В следующей программе рекурсивно определяется домен элементов списка, включающий как атомарные элементы, так и вложенные списки. Аргументом функтора list/1 является список элементов исходного домена. Атомы — это термы с функтором at/1. В программе реализуется операция линеаризации списка — приведения его к списку атомарных элементов. Идея определения операции линеаризации с помощью вспомогательного списка, играющего роль стека, описана в [9].

    domains
        elem{A} = at(A); list(elem{A}*).
    
    class predicates
        flatten: (elem{A}*) -> elem{A}*.
        flatten: (elem{A}*, elem{A}**) -> elem{A}*.
    clauses
        flatten(List) = flatten(List, []).
    
        flatten([list(L) | Tail], AuxList) =  flatten(L, [Tail | AuxList]):- !.
        flatten([Head | Tail], AuxList) =  [Head | flatten(Tail, AuxList)].
        flatten([], [L | AuxList]) =  flatten(L, AuxList).
        flatten([], []) = [].
    
        run():-
            L = [list([at(0)]), at(1), 
                    list([at(2), at(3), list([at(4), at(5), list([])])])],
            writef("%\n%", L, flatten(L)),
            _ = readLine().
    

    В языке Visual Prolog имеются развитые средства обработки исключений. Например, если функция определена только на непустых списках, то ее можно "доопределить" так, что она станет всюду определенной:

    class predicates
        first: (Elem*) -> Elem*.
    clauses
        first([H | _]) = [H].
        first([]) = _:-
            exception::raise_error(predicate_name(),
                "  Input list is empty.").
    

    Если аргумент предиката first равен пустому списку, то возбуждается исключение. Предикат predicate_name возвращает имя предиката, в определении которого он участвует.

    Упражнения

  • Проверьте, является ли список палиндромом.
  • Вычислите среднее арифметическое элементов списка, состоящего из целых чисел.
  • Проверьте, каждый ли элемент первого списка, содержится во втором списке.
  • Проверьте, является ли список упорядоченным по возрастанию или по убыванию.
  • Найдите позицию первого вхождения подсписка в список.
  • Вычислите все позиции заданного элемента в списке.
  • Разбейте список на отрезки длиной по n элементов. Последний подсписок может содержать меньшее число элементов.
  • Поменяйте местами два элемента списка, стоящих на заданных позициях.
  • Вычислите номер заданного атомарного элемента в списке, содержащем вложенные списки (см. листинг 6.9).
  • Вернуться к учебному плану