Анонимная переменная и сопоставление

Урок про подчёркивание _ — способ сказать Prolog «здесь что-то есть, но мне всё равно что».

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

Когда вы пишете правила и факты, часто оказывается, что некоторые аргументы вам нужны лишь как «заполнитель»: они должны там быть для совпадения по форме, но само значение вас не интересует. Для этого в Prolog есть анонимная переменная. Без неё код пухнет от ненужных имён, а интерпретатор сыплет предупреждениями.

Зачем это нужно

Представьте факты о людях: person(ann, 30, moscow) — имя, возраст, город. Допустим, вам нужны только имена всех людей, а возраст и город безразличны. Придумывать им имена Age, City — лишняя работа, да ещё Prolog предупредит: «singleton variable» (переменная использована один раз — может, опечатка?). Анонимная переменная решает обе проблемы: коротко и без предупреждений.

Как пишется _

Одиночное подчёркивание _ — это анонимная переменная. Она унифицируется с чем угодно, но её значение никуда не сохраняется.

?- person(Name, _, _) = person(ann, 30, moscow).
Name = ann.        % имя получили, возраст и город отброшены

Здесь Name мы попросили вернуть, а на месте возраста и города поставили _ — «неважно». Сравните с альтернативой, где пришлось бы ввести Age и City и потом их игнорировать.

Каждое _ — независимая переменная

Ключевой и неочевидный момент: разные вхождения _ в одном правиле — это РАЗНЫЕ переменные. Они не связаны между собой, в отличие от именованных переменных.

?- pair(_, _) = pair(1, 2).
true.              % первое _ стало 1, второе _ стало 2 — конфликта нет

?- pair(X, X) = pair(1, 2).
false.             % X должен быть и 1, и 2 одновременно — невозможно

В первом запросе два _ независимы, поэтому 1 и 2 спокойно «улеглись». Во втором одна и та же X обязана быть равной и 1, и 2 — а это противоречие, отсюда false. Вот почему важно различать «мне всё равно для каждой позиции отдельно» (_) и «здесь должно быть одно и то же значение» (X).

Паттерн-матчинг по структуре

Унификация с _ — основа сопоставления с образцом. Вы описываете форму терма, фиксируете нужное и игнорируете остальное.

% факты: книга(Название, Автор, Год, Жанр)
book('Война и мир', tolstoy, 1869, roman).
book('Идиот', dostoevsky, 1869, roman).
book('Метель', pushkin, 1831, povest).

% правило: автор любой книги 1869 года
author_1869(Author) :- book(_, Author, 1869, _).

В правиле author_1869 название и жанр заменены на _ (неважно), год жёстко зафиксирован числом 1869 (должен совпасть), а автор связывается с Author (его и вернём). Запрос отработает так:

?- author_1869(A).
A = tolstoy ;
A = dostoevsky.

Вывод:

A = tolstoy ;
A = dostoevsky.

Книга Пушкина не подошла — год 1831 не унифицировался с 1869, и Prolog её отбросил.

Списки и _

Особенно удобно с _ работать со списками. Например, «первый элемент списка» — нам неважен хвост:

first(X, [X|_]).        % голова — это X, хвост неважен

?- first(F, [10, 20, 30]).
F = 10.

second(X, [_, X|_]).    % первый неважен, второй — это X, остаток неважен

?- second(S, [10, 20, 30]).
S = 20.

Конструкция [X|_] читается как «список, у которого голова X, а хвост — какой угодно». Здесь _ избавляет от ненужного имени для хвоста.

Как работает под капотом

Когда парсер Prolog встречает _, он создаёт новую свежую переменную при каждом вхождении. То есть foo(_, _) внутри превращается в нечто вроде foo(_G1, _G2) с разными внутренними именами. Поэтому два _ и не связаны. Дальше эта свежая переменная унифицируется как обычно, но поскольку на неё больше никто не ссылается, её связанное значение немедленно становится «мусором» — отсюда и то, что значение «не сохраняется».

   first(F, [10, 20, 30])
            │
   сопоставляем с first(X, [X|_])
            │
   F = X,  [10,20,30] = [X|_]
            │
   X = 10 (голова),  _ = [20,30] (хвост — отброшен)
            │
         F = 10

А именованная переменная, использованная лишь раз (например Tail в [X|Tail], где Tail больше нигде нет), вызывает предупреждение «singleton». Prolog подсказывает: «возможно, опечатка — может, ты хотел _?» Если значение правда не нужно — меняйте на _ или на _Tail (имя с подчёркивания тоже глушит предупреждение, но при этом читается).

Частые ошибки

  • Думать, что все _ равны. p(_, _) — это НЕ p(X, X). Каждое подчёркивание независимо; чтобы потребовать одинаковости, нужна именованная переменная.
  • Пытаться использовать значение _. Из _ ничего не вернуть — оно безымянно. Если значение нужно дальше, дайте переменной имя.
  • Игнорировать предупреждение singleton. Оно часто ловит реальные опечатки в именах. Осознанно неиспользуемую переменную помечайте _ или префиксом _.
  • Ставить _ там, где нужна связь. В правиле «у X и Y один родитель» обе позиции родителя должны быть одной переменной, а не двумя _.

Итоги

  • _ — анонимная переменная: унифицируется с чем угодно, значение не сохраняется.
  • Каждое вхождение _ — отдельная свежая переменная; они между собой не связаны.
  • _ — инструмент паттерн-матчинга: фиксируем нужные позиции, игнорируем ненужные.
  • В списках [X|_] удобно брать голову, не называя хвост.
  • Предупреждение «singleton variable» — повод заменить одноразовую переменную на _ (если значение действительно не нужно).
Проверьте себя
1. Каков результат запроса ?- pair(_, _) = pair(1, 2).
Afalse — нельзя, чтобы _ было и 1, и 2
Btrue — два подчёркивания независимы, унифицируются с 1 и 2
Ctrue, но _ запомнит только последнее значение 2
DОшибка: _ нельзя использовать дважды
2. Чем _ отличается от именованной переменной X?
AНичем, это просто другое написание
B_ унифицируется с чем угодно, но не сохраняет значение и каждое вхождение независимо
C_ можно использовать только в списках
D_ всегда равно нулю
3. Что вернёт правило first(X, [X|_]) на запросе ?- first(F, [10, 20, 30]).
AF = [20, 30]
BF = 10
CF = [10, 20, 30]
Dfalse
4. Почему именованная переменная, использованная лишь раз, вызывает предупреждение «singleton»?
AПотому что Prolog запрещает однократные переменные
BПотому что это часто опечатка в имени; если значение не нужно, стоит писать _
CПотому что singleton-переменные замедляют программу
DПотому что такие переменные нельзя унифицировать