Как преобразовать состояние возврата Prolog для выполнения той же задачи, что и lazy seq из Clojure?

Вот алгоритм быстрой сортировки чисел, написанных на Clojure. По сути, это алгоритм быстрой сортировки из "The Joy of Clojure", 2-е издание, стр. 133. Я немного изменил его для (надеюсь) лучшей читаемости, потому что оригинал казался слишком компактным:

(defn qsort-inner [work]
   (lazy-seq        
      (loop [loopwork work]
         (let [[ part & partz ] loopwork ]
            (if-let [[pivot & valuez] (seq part)]
                  (let [ smaller? #(< % pivot)
                         smz      (filter smaller? valuez)
                         lgz      (remove smaller? valuez)
                         nxxt     (list* smz pivot lgz partz) ]
                        (recur nxxt))
                  (if-let [[oldpivot & rightpartz] partz]
                          (cons oldpivot (qsort-inner rightpartz))
                          []))))))

(defn qsort [ xs ]
   (qsort-inner (list xs)))

Алгоритм запускается вызовом qsort, который включает переданный список номеров в другой список (таким образом, создается список, содержащий единственный список), а затем вызывает qsort-inner.

(qsort [10 4 5 88 7 1])     ;; (qsort-inner [[10 4 5 88 7 1]])
;; (1 4 5 7 10 88)

qsort-inner имеет три важных момента:

  • Это задерживает фактическую обработку. Вместо того, чтобы возвращать результат полной сортировки входного списка, он возвращает «lazy-seq», который является (object? Thing? thunk?), который выдает следующий номер отсортированной последовательности при запросе, т. е. сортирует по мере необходимости. Состояние вычисления определяется подвешенным хвостом (cons oldpivot (qsort-inner rightpartz))
  • Существует loop + recur хвостовая рекурсивная часть, которая используется всякий раз, когда алгоритм перемещается вниз по дереву сортировки "влево" (подробности алгоритма см. Ниже).
  • Существует полностью рекурсивный вызов (qsort-inner rightpartz), который используется, когда было получено следующее наименьшее число, и дерево сортировки может быть «переупорядочено» (подробности алгоритма см. Ниже).

С помощью вещи lazy-seq мы можем заставить алгоритм выдавать данные один за другим:

;; the full result is generated on printout
(qsort [10 4 5 88 7 1])
(1 4 5 7 10 88)

;; store the lazy-seq and query it
(def l (qsort [10 4 5 88 7 1]))
(first l)
;; 1
(second l)
;; 4

Я думал о том, как выполнить эту ленивую быструю сортировку на Прологе. Фактически, лень, по крайней мере в этом случае, дается в Prolog бесплатно путем возврата! Мы можем запросить первый результат, вычисление останавливается, а следующий результат получается с помощью поиска с возвратом.

qsort_inner(X, [[],X|_]).
qsort_inner(X, [[],_|WorkRest]) :- qsort_inner(X, WorkRest).
qsort_inner(X, [[Piv|Ns]|WorkRest]) :- 
    pick_smaller(Piv,Ns,SMs),
    pick_notsmaller(Piv,Ns,NSMs),
    qsort_inner(X,[SMs,Piv,NSMs|WorkRest]).

pick_smaller(Pivot,Ins,Outs) :- include(@>(Pivot),Ins,Outs).
pick_notsmaller(Pivot,Ins,Outs) :- exclude(@>(Pivot),Ins,Outs).

qsort(X,Lin) :- qsort_inner(X,[Lin]).

Сортировать список «лениво»:

qsort(X,[3,2,1]).
X = 1;
X = 2;
X = 3;
false

Должен получить их все:

qsort_fully(Lin,Lout) :- bagof(X, qsort(X, Lin), Lout).

К сожалению, структура данных, отслеживающая вычислительное состояние, не очевидна: она находится в стеке и не может быть объединена с переменной. Таким образом, я могу использовать эту «лень» только тогда, когда нахожусь на верхнем уровне Prolog.

Как мне зафиксировать состояние вычисления и вызвать его позже?

Обратите внимание на принцип работы быстрой сортировки

  • Учитывая список чисел, алгоритм выбирает первый элемент списка в качестве значения поворота (светло-зеленый на изображении).
  • Затем он строит дерево с этими числами, которые строго меньше, чем значение точки поворота в списке «слева», самой точкой поворота (темно-зеленый) и числами, большими или равными значению поворота, в виде списка «справа».
  • Затем он рекурсивно перемещается по дереву «влево».
  • Это продолжается до тех пор, пока список чисел, меньших, чем значение поворота, не станет пустым.
  • В этот момент значение поворота (здесь 28) является наименьшим числом в целом и может быть выведено.
  • Это делает список для сортировки на один элемент меньше. Теперь дерево можно уменьшить на один уровень с помощью тривиальной операции перестановки: правая ветвь «самого глубокого дерева-узла, но без поворота» теперь без левой ветки и без поворота становится левой ветвью узла дерева «самое глубокое дерево». узел кроме двух ".
  • Теперь поиск наименьшего элемента можно снова продолжить «влево».

Древовидную структуру не нужно явно сохранять, поскольку она не содержит информации. Вместо этого в списке сохраняется последовательность чередующихся «листовых списков» и «сводных номеров». Поэтому мы и создали изначальный «список чисел».

Частичный пример запуска быстрой сортировки


person David Tonhofer    schedule 16.07.2019    source источник
comment
Я прочитал это и думаю, что вы на самом деле не задали вопрос о проблеме, в решении которой вам нужна помощь. Вы догадывались, что могло бы решить проблему, и приводили примеры, но этот пример - лучший способ понять идею, находящуюся в вашей голове в настоящее время. Сколько раз я думаю, что reify - это ответ, оказывается, что последнее на самом деле не нужно. Иногда ответ - использовать BFS вместо DFS по умолчанию. Иногда нужно использовать список различий, который проще сделать как DCGS.   -  person Guy Coder    schedule 16.07.2019
comment
Возможно, у одного из них есть ответ, который вы ищете: Разница Списки Фрэнка Пфеннинга или алгоритмы сортировки, реализованные в Prolog Маркусом Триска   -  person Guy Coder    schedule 16.07.2019
comment
Интересно: lazy_lists.pl   -  person Guy Coder    schedule 16.07.2019
comment
Я рекомендую Ленивые списки в Прологе? с двумя подходами - более задействованный с генераторами с отслеживанием состояния (отказ от ответственности, этот ответ принадлежит мне) и более прямой и простой с freeze/2 (также gist.github.com/mndrix/4644762 < / а>). Или какая-то их комбинация. :)   -  person Will Ness    schedule 16.07.2019
comment
@DanielLyons next/3 - также необходимо текущее состояние. :) (Думаю, не так уж и грязно.)   -  person Will Ness    schedule 16.07.2019
comment
@WillNess Nice!   -  person Daniel Lyons    schedule 16.07.2019
comment
@DanielLyons, спасибо. :) Я тогда не знал о freeze/2. пришлось обойтись без этого. :)   -  person Will Ness    schedule 16.07.2019
comment
Хорошие комментарии. Хорошие ответы. Придется выбирать ...   -  person David Tonhofer    schedule 16.07.2019


Ответы (2)


Пролог - очень реифицируемый язык. Просто превратите свой код в данные:

qsort_gen(Lin, G) :- 
    % G is the initial generator state for Lin's quicksorting
    G = qsort_inner([Lin]).

    %    This_State                   Next_Elt      Next_State
next( qsort_inner([[], X    | WorkRest]), X, qsort_inner(WorkRest) ).
next( qsort_inner([[Piv|Ns] | WorkRest]), X, G ) :-
    pick_smaller(  Piv, Ns, SMs),
    pick_notsmaller(Piv, Ns, NSMs),
    next( qsort_inner([SMs, Piv, NSMs | WorkRest]), X, G).

pick_smaller(  Pivot, Ins, Outs) :- include( @>(Pivot), Ins, Outs).
pick_notsmaller(Pivot, Ins, Outs) :- exclude( @>(Pivot), Ins, Outs).

Это все.

15 ?- qsort_gen([3,2,5,1,9,4,8], G), next(G,X,G2), next(G2,X2,G3), next(G3,X3,G4).
G = qsort_inner([[3, 2, 5, 1, 9, 4, 8]]),
X = 1,
G2 = qsort_inner([[], 2, [], 3, [5, 9, 4|...]]),
X2 = 2,
G3 = qsort_inner([[], 3, [5, 9, 4, 8]]),
X3 = 3,
G4 = qsort_inner([[5, 9, 4, 8]]).

16 ?- qsort_gen([1,9,4,8], G), next(G,X,G2), next(G2,X2,G3), next(G3,X3,G4).
G = qsort_inner([[1, 9, 4, 8]]),
X = 1,
G2 = qsort_inner([[9, 4, 8]]),
X2 = 4,
G3 = qsort_inner([[8], 9, []]),
X3 = 8,
G4 = qsort_inner([[], 9, []]).

17 ?- qsort_gen([1,9,4], G), next(G,X,G2), next(G2,X2,G3), next(G3,X3,G4).
G = qsort_inner([[1, 9, 4]]),
X = 1,
G2 = qsort_inner([[9, 4]]),
X2 = 4,
G3 = qsort_inner([[], 9, []]),
X3 = 9,
G4 = qsort_inner([[]]).

Для упрощения взаимодействия мы можем использовать take/4:

take( 0, Next, Z-Z, Next):- !.
take( N, Next, [A|B]-Z, NextZ):- N>0, !, next( Next, A, Next1),
  N1 is N-1,
  take( N1, Next1, B-Z, NextZ).

Потом,

19 ?- qsort_gen([3,2,5,1,9,4,8], G), take(6, G, L-[], _).
G = qsort_inner([[3, 2, 5, 1, 9, 4, 8]]),
L = [1, 2, 3, 4, 5, 8].

20 ?- qsort_gen([3,2,5,1,9,4,8], G), take(7, G, L-[], _).
G = qsort_inner([[3, 2, 5, 1, 9, 4, 8]]),
L = [1, 2, 3, 4, 5, 8, 9].

21 ?- qsort_gen([3,2,5,1,9,4,8], G), take(10, G, L-[], _).
false.

take/4, очевидно, нужно настроить, чтобы корректно закрыть список вывода при сбое next/3. Первоначально он был написан с расчетом на бесконечные списки. Это оставлено как упражнение для увлеченного исследователя.

person Will Ness    schedule 16.07.2019
comment
Отлично! Я просматривал prost.pl Маркуса Триски поздно ночью, но не нажимал . - person David Tonhofer; 16.07.2019
comment
Я так долго использую остроконечные {} языки, что мое восприятие кода как данных атрофировалось. Это заговор. - person David Tonhofer; 16.07.2019
comment
Пролог особенный. Я думаю, что в этом отношении он гораздо более Lispier, чем Lisp. (Мне нравится иногда говорить, что компиляция была ошибкой на триллион долларов. Но на самом деле это не компиляция как таковая, а распространение двоичных файлов без исходных текстов. ИМО.) - person Will Ness; 16.07.2019

Это не стандартизовано, но ряд Прологов в настоящее время предоставляют средства для поддержки и управления несколькими независимыми состояниями с возвратом, часто известными как механизмы.

Например, используя соответствующие примитивы в ECLiPSe, можно написать

init(Vars, Goal, Engine) :-
    engine_create(Engine, []),
    engine_resume(Engine, (true;Goal,yield(Vars,Cont),Cont), true).

more(Engine, Vars) :-
    engine_resume(Engine, fail, yielded(Vars)).

и используйте это следующим образом (с qsort/2, как определено вами)

?- init(X, qsort(X,[3,2,1]), Eng),
   more(Eng, X1),
   more(Eng, X2),
   more(Eng, X3).

X = X
Eng = $&(engine,"9wwqv3")
X1 = 1
X2 = 2
X3 = 3
Yes (0.00s cpu)

Здесь переменная Eng привязана к непрозрачной ручке engine. Этот механизм выполняет недетерминированную цель, которая дает новое решение вызывающей стороне каждый раз, когда она возобновляется и получает указание на возврат.

person jschimpf    schedule 16.07.2019
comment
SWI-Prolog, похоже, имеет нечто подобное с использованием engine_create/3 и engine_next/2 (документы API). - person Daniel Lyons; 16.07.2019