Введение
Логическое программирование с удовлетворением ограничений
Программирование с ограничениями — это форма программирования с удовлетворением ограничений, в которой логическое программирование расширяется за счет включения концепций из области удовлетворения ограничений. Логическая программа с ограничениями — это логическая программа, содержащая ограничения в теле клауз. Примером клаузы, включающей ограничение, является: В этой клаузе является ограничением, а A(X,Y), B(X) и C(Y) — литералы, как и в обычном логическом программировании. Эта клауза определяет одно условие, при котором утверждение A(X,Y) истинно: X+Y больше нуля, и B(X) и C(Y) также истинны. Как и в обычном логическом программировании, программы опрашиваются относительно доказуемости цели, которая сама по себе может содержать ограничения в дополнение к литералам. Доказательство цели состоит из клауз, тела которых содержат выполнимые ограничения и литералы, которые, в свою очередь, могут быть доказаны с использованием других клауз. Выполнение осуществляется интерпретатором, который начинает с цели и рекурсивно просматривает клаузы, пытаясь доказать цель. Ограничения, обнаруженные в процессе просмотра, помещаются в множество, называемое хранилищем ограничений. Если это множество признается невыполнимым, интерпретатор осуществляет возврат, пытаясь использовать другие клаузы для доказательства цели. На практике выполнимость хранилища ограничений может проверяться с помощью неполного алгоритма, который не всегда обнаруживает противоречия.
Обзор
Формально, программы с ограничениями логического программирования похожи на обычные программы логического программирования, но тело правил может содержать ограничения, в дополнение к обычным логическим литералам. Например, X>0 является ограничением и включено в последнее правило следующей программы с ограничениями логического программирования. B(X,1): X<0. B(X,Y): X=1, Y>0. A(X,Y): X>0, B(X,Y). Как и в обычном логическом программировании, вычисление цели, такой как A(X,1), требует вычисления тела последнего правила при Y=1. Как и в обычном логическом программировании, это, в свою очередь, требует доказательства цели B(X,1). В отличие от обычного логического программирования, для этого также требуется выполнение ограничения: X>0, ограничения в теле последнего правила. (В обычном логическом программировании X>0 нельзя доказать, если X не связано с полностью определенным термом, и выполнение программы завершится неудачей, если это не так.) Не всегда можно определить, выполнено ли ограничение, когда оно встречается. В этом случае, например, значение X не определено при вычислении последнего правила. В результате, ограничение X>0 в данный момент не выполняется и не нарушается. Вместо того, чтобы продолжить вычисление B(X,1) и затем проверить, является ли полученное значение X положительным, интерпретатор сохраняет ограничение X>0 и затем продолжает вычисление B(X,1); таким образом, интерпретатор может обнаружить нарушение ограничения X>0 во время вычисления B(X,1) и немедленно выполнить откат, если это произойдет, а не ждать завершения вычисления B(X,1). В целом, вычисление программы с ограничениями логического программирования происходит так же, как и в обычной программе логического программирования. Однако ограничения, встречающиеся в процессе вычисления, помещаются в множество, называемое хранилищем ограничений. В качестве примера, вычисление цели A(X,1) продолжается путем вычисления тела первого правила при Y=1; это вычисление добавляет X>0 в хранилище ограничений и требует доказательства цели B(X,1). При попытке доказать эту цель, применяется первое правило, но его вычисление добавляет X<0 в хранилище ограничений. Это добавление делает хранилище ограничений неудовлетворимым. Затем интерпретатор выполняет откат, удаляя последнее добавление из хранилища ограничений. Вычисление второго правила добавляет X=1 и Y>0 в хранилище ограничений. Поскольку хранилище ограничений удовлетворимо и не осталось других литералов для доказательства, интерпретатор останавливается с решением X=1, Y=1.
Условия и положения
Используются различные определения терминов, что приводит к возникновению различных видов логического программирования с ограничениями: над деревьями, действительными числами или конечными областями. Ограничение равенства терминов присутствует всегда. Такие ограничения необходимы, поскольку интерпретатор добавляет t1=t2 к цели всякий раз, когда литерал P(t1) заменяется телом нового варианта клаузы, головой которой является P(t2).
Термины дерева
Логическое программирование с ограничениями с использованием древовидных термов эмулирует обычное логическое программирование, сохраняя подстановки в виде ограничений в хранилище ограничений. Термы – это переменные, константы и символы функций, применяемые к другим термам. Рассматриваются только ограничения равенства и неравенства между термами. Равенство особенно важно, поскольку такие ограничения, как t1=t2, часто генерируются интерпретатором. Ограничения равенства термов могут быть упрощены, то есть решены, посредством унификации:
Ограничение t1=t2 может быть упрощено, если оба терма являются символами функций, применяемыми к другим термам. Если эти два символа функций одинаковы, и количество подтермов также одинаково, это ограничение можно заменить на попарное равенство подтермов. Если термы состоят из разных символов функций или одного и того же функтора, но примененного к разному числу термов, ограничение будет неудовлетворимым. Если один из двух термов является переменной, единственным допустимым значением для этой переменной является другой терм. В результате другой терм может заменить переменную в текущей цели и хранилище ограничений, фактически исключая переменную из рассмотрения. В частности, в случае равенства переменной самой себе, ограничение можно удалить, поскольку оно всегда выполнено. В этой форме решения ограничений значениями переменных являются термы.
Реал
Логическое программирование с использованием действительных чисел использует действительные выражения в качестве термов. Когда символы функций не используются, термы являются выражениями над действительными числами, возможно, включая переменные. В этом случае каждая переменная может принимать только действительное число в качестве значения. Точнее говоря, термы – это выражения над переменными и действительными константами. Равенство между термами – это вид ограничения, которое всегда присутствует, поскольку интерпретатор генерирует равенства термов во время выполнения. Например, если первый литерал текущей цели – A(X+1), а интерпретатор выбрал пункт A(Y 1): Y=1 после переписывания переменных, то ограничения, добавленные к текущей цели, будут X+1=Y 1, и правила упрощения, используемые для символов функций, очевидно, не применяются: X+1=Y 1 не является противоречивым только потому, что первое выражение построено с использованием +, а второе – с использованием действительных чисел. Действительные числа и символы функций могут быть объединены, что приводит к термам, являющимся выражениями над действительными числами и символами функций, применяемыми к другим термам. Формально, переменные и действительные константы являются выражениями, как и любой арифметический оператор над другими выражениями. Переменные, константы (символы функций нулевой арности) и выражения являются термами, как и любой символ функции, применяемый к термам. Другими словами, термы строятся на основе выражений, а выражения – на основе чисел и переменных. В этом случае область значений переменных включает действительные числа и термы. Иными словами, одна переменная может принимать действительное число в качестве значения, а другая – терм. Равенство двух термов можно упростить, используя правила для термов в виде деревьев, если ни один из термов не является действительным выражением. Например, если два терма имеют один и тот же символ функции и количество подтермов, их ограничение равенства можно заменить на равенство подтермов.
Reals and function symbols can be combined, leading to terms that are expressions over reals and function symbols applied to other terms. Formally, variables and real constants are expressions, as any arithmetic operator over other expressions. Variables, constants (zero arity function symbols), and expressions are terms, as any function symbol applied to terms. In other words, terms are built over expressions, while expressions are built over numbers and variables. In this case, variables ranges over real numbers and terms. In other words, a variable can take a real number as a value, while another takes a term. Equality of two terms can be simplified using the rules for tree terms if none of the two terms is a real expression. For example, if the two terms have the same function symbol and number of subterms, their equality constraint can be replaced with the equality of subterms.
Ограниченные домены
Третий класс ограничений, используемых в логическом программировании с ограничениями, – это конечные домены. В этом случае значения переменных берутся из конечного домена, часто состоящего из целых чисел. Для каждой переменной можно указать свой домен: например, X::[1 5] означает, что значение X находится между 1 и 5. Домен переменной также можно задать, перечислив все возможные значения, которые может принимать переменная; таким образом, вышеуказанное объявление домена можно также записать как X::[1,2,3,4,5]. Этот второй способ задания домена позволяет использовать домены, не состоящие из целых чисел, например X::[george,mary,john]. Если домен переменной не указан, предполагается, что это множество целых чисел, представимых в данном языке. Группе переменных можно присвоить один и тот же домен с помощью объявления вида [X,Y,Z]::[1 5]. Домен переменной может быть сужен в процессе выполнения. Действительно, когда интерпретатор добавляет ограничения в хранилище ограничений, он выполняет распространение ограничений для обеспечения некоторой формы локальной согласованности, и эти операции могут уменьшить домен переменных. Если домен переменной становится пустым, хранилище ограничений становится несогласованным, и алгоритм откатывается. Если домен переменной становится сингулярным (содержит единственный элемент), переменной можно присвоить единственное значение из её домена. Обычно применяемые формы согласованности – это согласованность по дуге, гипер-согласованность и согласованность по границам. Текущий домен переменной можно проверить, используя специальные литералы; например, dom(X,D) определяет текущий домен D переменной X. Что касается доменов вещественных чисел, то функторы могут использоваться вместе с доменами целых чисел. В этом случае терм может быть выражением над целыми числами, константой или применением функтора к другим термам. Переменная может принимать произвольный терм в качестве значения, если её домен не был явно указан как множество целых чисел или констант.
Хранилище ограничений
Хранилище ограничений содержит ограничения, которые в настоящий момент считаются выполнимыми. Его можно рассматривать как текущую подстановку в обычном логическом программировании. Когда разрешены только термы-деревья, хранилище ограничений содержит ограничения вида t1=t2; эти ограничения упрощаются унификацией, в результате чего получаются ограничения вида переменная=терм; такие ограничения эквивалентны подстановке. Однако хранилище ограничений может также содержать ограничения вида t1!=t2, если допускается неравенство != между термами. Когда разрешены ограничения над вещественными числами или конечными областями, хранилище ограничений может также содержать специфичные для области ограничения, такие как X+2=Y/2 и т.п. Хранилище ограничений расширяет понятие текущей подстановки двумя способами. Во-первых, оно содержит не только ограничения, полученные из отождествления литерала с головой свежей вариации клоза, но и ограничения тела клозов. Во-вторых, оно содержит не только ограничения вида переменная=значение, но и ограничения, относящиеся к рассматриваемому языку ограничений. В то время как результатом успешного вычисления обычной логической программы является окончательная подстановка, результатом для логической программы с ограничениями является окончательное хранилище ограничений, которое может содержать ограничения вида переменная=значение, но в общем случае может содержать произвольные ограничения. Специфичные для области ограничения могут поступать в хранилище ограничений как из тела клозов, так и из отождествления литерала с головой клоза: например, если интерпретатор переписывает литерал A(X+2) с клозом, голова свежей вариации которого A(Y/2), то ограничение X+2=Y/2 добавляется в хранилище ограничений. Если переменная встречается в выражении над вещественными числами или конечной областью, она может принимать только значение из области вещественных чисел или конечной области. Такая переменная не может принимать в качестве значения терм, составленный из функтора, примененного к другим термам. Хранилище ограничений становится невыполнимым, если переменная обязана принимать как значение из конкретной области, так и функтор, примененный к термам. После добавления ограничения в хранилище ограничений над ним выполняются некоторые операции. Какие именно операции выполняются, зависит от рассматриваемой области и ограничений. Например, унификация используется для конечных равенств термов-деревьев, исключение переменных – для полиномиальных уравнений над вещественными числами, распространение ограничений – для обеспечения формы локальной согласованности для конечных областей. Эти операции направлены на упрощение хранилища ограничений для проверки на выполнимость и решения. В результате этих операций добавление новых ограничений может изменить старые. Важно, чтобы интерпретатор мог отменять эти изменения при возврате. Самый простой способ – сохранять полное состояние хранилища каждый раз, когда делается выбор (выбирается клоз для переписывания цели). Существуют более эффективные методы, позволяющие хранилищу ограничений возвращаться в предыдущее состояние. В частности, можно сохранять только изменения, внесенные в хранилище ограничений между двумя точками выбора, включая изменения, внесенные в старые ограничения. Это можно сделать, просто сохраняя старое значение измененных ограничений; этот метод называется трассировкой (trailing). Более продвинутый метод – сохранять изменения, внесенные в измененные ограничения. Например, линейное ограничение изменяется путем изменения его коэффициента: сохранение разницы между старым и новым коэффициентом позволяет отменить изменение. Этот второй метод называется семантической трассировкой (semantic backtracking), поскольку сохраняется семантика изменения, а не только старая версия ограничений.
because the semantics of the change is saved rather than the old version of the constraints only.
Маркировка
Литералы маркировки используются для переменных с конечными доменами для проверки выполнимости или частичной выполнимости хранилища ограничений и для поиска выполнимого назначения. Литерал маркировки имеет вид `labeling([переменные])`, где аргументом является список переменных с конечными доменами. Каждый раз, когда интерпретатор вычисляет такой литерал, он выполняет поиск по доменам переменных из списка, чтобы найти назначение, удовлетворяющее всем релевантным ограничениям. Обычно это делается с помощью обратного отслеживания: переменные вычисляются последовательно, перебирая все возможные значения для каждой из них и осуществляя откат при обнаружении противоречия. Первое применение литерала маркировки – это фактическая проверка выполнимости или частичной выполнимости хранилища ограничений. Когда интерпретатор добавляет ограничение в хранилище ограничений, он обеспечивает только форму локальной согласованности. Эта операция может не обнаружить противоречие, даже если хранилище ограничений невыполнимо. Литерал маркировки над набором переменных обеспечивает проверку выполнимости ограничений для этих переменных. Следовательно, использование всех переменных, упомянутых в хранилище ограничений, приводит к проверке выполнимости всего хранилища. Второе применение литерала маркировки – это фактическое определение значения переменных, удовлетворяющего хранилищу ограничений. Без литерала маркировки переменным присваиваются значения только тогда, когда хранилище ограничений содержит ограничение вида `X=значение` и когда локальная согласованность сужает домен переменной до единственного значения. Литерал маркировки над некоторыми переменными заставляет эти переменные быть вычисленными. Иными словами, после рассмотрения литерала маркировки всем переменным присваивается значение. Обычно программы логики ограничений пишутся таким образом, что литералы маркировки вычисляются только после накопления в хранилище ограничений максимально возможного количества ограничений. Это связано с тем, что литералы маркировки обеспечивают поиск, а поиск эффективнее, если существует больше ограничений, которые необходимо удовлетворить. Задача об удовлетворимости ограничений обычно решается программой логики ограничений со следующей структурой:
Когда интерпретатор вычисляет цель `solve(аргументы)`, он помещает тело свежего варианта первого правила в текущую цель. Поскольку первая цель – `constraints(X')`, вычисляется второе правило, и эта операция перемещает все ограничения из текущей цели и, в конечном итоге, в хранилище ограничений. Затем вычисляется литерал `labeling(X')`, что заставляет выполнить поиск решения хранилища ограничений. Поскольку хранилище ограничений содержит ровно ограничения исходной задачи об удовлетворимости ограничений, эта операция ищет решение исходной задачи.
Переформулирование программы
Данная программа с логикой ограничений может быть переформулирована для повышения ее эффективности. Первое правило заключается в том, что литералы с разметкой (labeling literals) следует размещать после того, как в хранилище ограничений накопится как можно больше ограничений на эти литералы. Хотя в теории эквивалентно , поиск, выполняемый интерпретатором при встрече с литералом разметки, осуществляется в хранилище ограничений, которое не содержит ограничения X>0. В результате он может генерировать решения, такие как X=1, которые впоследствии оказываются не удовлетворяющими этому ограничению. С другой стороны, во второй формулировке поиск выполняется только тогда, когда ограничение уже присутствует в хранилище ограничений. В результате поиск возвращает только решения, согласованные с ним, используя тот факт, что дополнительные ограничения уменьшают пространство поиска. Вторая переформулировка, способная повысить эффективность, заключается в размещении ограничений перед литералами в теле правила. Опять же, и в принципе эквивалентны. Однако первый вариант может потребовать больше вычислений. Например, если хранилище ограничений содержит ограничение X<2, интерпретатор рекурсивно вычисляет B(X) в первом случае; если вычисление успешно, он затем обнаруживает, что хранилище ограничений становится несовместимым при добавлении X>0. Во втором случае, при вычислении этого правила, интерпретатор сначала добавляет X>0 в хранилище ограничений, а затем, возможно, вычисляет B(X). Поскольку хранилище ограничений после добавления X>0 оказывается несовместимым, рекурсивное вычисление B(X) не выполняется вообще. Третья переформулировка, способная повысить эффективность, – это добавление избыточных ограничений. Если программист знает (каким бы способом) что решение задачи удовлетворяет определенному ограничению, он может включить это ограничение, чтобы как можно быстрее вызвать несовместимость хранилища ограничений. Например, если заранее известно, что вычисление B(X) приведет к положительному значению для X, программист может добавить X>0 перед любым использованием B(X). Например, A(X,Y): B(X),C(X) не сможет достичь цели A(2,Z), но это выясняется только при вычислении подцели B(X). С другой стороны, если вышеуказанное правило заменить на , интерпретатор выполнит откат (backtrack) сразу после добавления ограничения X>0 в хранилище ограничений, что происходит до начала вычисления B(X).
Программирование логики с одновременными ограничениями
Одновременные версии логического программирования с ограничениями ориентированы на программирование параллельных процессов, а не на решение задач об удовлетворении ограничениям. Цели в логическом программировании с ограничениями вычисляются параллельно, поэтому параллельный процесс программируется как вычисление цели интерпретатором. Синтаксически, программы логического программирования с одновременными ограничениями похожи на непараллельные программы, за исключением того, что клаузы включают охранные условия – ограничения, которые могут блокировать применимость клаузы при определенных условиях. Семантически, логическое программирование с одновременными ограничениями отличается от непараллельных версий тем, что вычисление цели предназначено для реализации параллельного процесса, а не для поиска решения задачи. Наиболее заметно, это различие влияет на поведение интерпретатора, когда применимо несколько клауз: непараллельное логическое программирование с ограничениями рекурсивно перебирает все клаузы, а параллельное логическое программирование с ограничениями выбирает только одну. Это наиболее явный эффект заданной направленности интерпретатора, который никогда не пересматривает ранее принятое решение. Другие последствия этого – семантическая возможность наличия цели, которую нельзя доказать, при этом всё вычисление не завершается неудачей, и особый способ сопоставления цели и заголовка клаузы.
Приложения
Логическое программирование с ограничениями находит применение в различных областях, таких как автоматизированное планирование, вывод типов, гражданское и машиностроение, верификация цифровых схем, управление воздушным движением, финансы и другие.
История
Ограниченное логическое программирование было введено Джаффаром и Лассесом в 1987 году. Они обобщили наблюдение о том, что уравнения и неравенства термов в Prolog II являлись частным случаем ограничений, и распространили эту идею на произвольные языки ограничений. Первыми реализациями этой концепции стали Prolog III, CLP(R) и CHIP.