Мысли вслух. Необратимое перекрытие идентификаторов.
Перекрытие1 (англ. overlapping) идентификаторов известно еще с времен первых языков программирования. Меня волнует тот факт что подобная проблема дожила до наших дней и сегодня встречается почти в любом, даже современном языке. Я решил написать на эту тему статью, в которой и попытаюсь высказать свои мысли на этот счет.
Проблематика
Пример кода №1
Переменная перекрывает доступ к аргументу
function f(x = 1) {
var x = 2;
print(x); // 2
print(???); // как получить х = 1?
}
В коде выше (см. Пример кода №1) переменная х перекрывает аргумент х. После ее объявления предыдущий объект теряется навсегда2. И это проблема, ведь одинаковые названия могут быть отнюдь не случайными. Если подобное именование будет нести некоторую полезную нагрузку, то механизм перекрытия превратиться в настоящую преграду.
Но почему не предусмотрен механизм для обращения к перекрытой переменной? Почему вообще перекрытие разрешено на уровне языка? В таком виде от него нет пользы, ведь переменные все ровно придется переименовывать если понадобиться одновременный доступ к обеим.
Жизнь без перекрытия
Существуют языки лишенные подобных недостатков. Правда использованная в них стратегия не столько развивает этот механизм, сколько ограничивает его применение. Например, в языке программирования C# перекрытие запрещено3. Переменные должны объявляться так, чтобы к ним можно было обратиться с любого места в программе (см. Пример кода №2). При этом сохраняется возможность перекрывать поля класса при наследовании, а так же аргументами методов. Подобные возможности не противоречат правилам, неоднозначности в этих случаях всегда можно разрешить (см. Пример кода №3). Это очень важный момент, ведь такое обратимое перекрытие не несет в себе явных недостатков, лишь преимущества4.
Пример кода №2
В C# попытка перекрыть переменную приведет к ошибке
int x = 5;
...
{
int x = 10; // ошибка
}
Пример кода №3
Обратимое перекрытие поля класса аргументом в C#
class X {
int x;
public void F(int x)
{
print(x); // аргумент
print(this.x); // поле класса
}
}
Неполные решения
В JavaScript, С++ и Python проблему перекрытия решили частично. Перекрытие локальных переменных по прежнему остается необратимым процессом. А вот обращение к глобальным объектам можно отличить при помощи специальных операторов. В С++ для этих целей используется оператор :: (см. Пример кода №4). Чтобы выполнить такое в Python переменная должна быть объявлена через ключевое слово global (см. Пример кода №5). А в JavaScript global выступает в роли специального объекта, который содержит в себе все глобальные переменные в качестве полей (см. Пример кода №6).
Пример кода №4
Обращение к глобальному объекту в С++ при помощи оператора ::
int x = 5;

void f() {
int x = 10;
print(::x); // 5
}
Пример кода №5
Работа с глобальной переменной в Python
x = 5

def f():
global x
x = 10

f()

print(x) # 10
Пример кода №6
Обращение через глобальный объект в JavaScript
var x = 5;

function f() {
var x = 10;
console.log(x); // 10
console.log(global.x); // 5
}

f();
Это хорошо иллюстрирует попытки борьбы с проблемой. Но использованные в них механизмы рассчитаны лишь на один уровень вложенности и перестают работать для перекрытий большего порядка (см. Пример кода №7). Возможно именно из-за низкой актуальности проблемы этот подход не был развит в что-то большее5. Но еще не поздно это исправить!
Пример кода №7
Оператор :: не сработает для переменной х с значением 2
int x = 1;

void f()
{
int x = 2;
{
int x = 3
print(x); // 3
print(::x); // 1
print(???); // 2?
}
}
Обращение в порядке объявления
Один из путей развития механизма перекрытия - обращение к перекрытым переменным через специальный префикс или ключевое слово. Причем с увеличением порядка перекрытия увеличивается и число таких префиксов (см. Пример кода №8). Такой подход вполне естественный и хорошо известен из роботы с файловой системой. Интересно, что с такими возможностями вообще нет смысла в ограничении на уникальность идентификатора в пределах области 6 (см. Пример кода №9).
Пример кода №8
Обращение в порядке объявления
int x = 1;

{
int x = 2;
{
int x = 3
print(x); // 3
print(../x); // 2
print(../../x); // 1
}
}
Пример кода №9
Обращение к последовательно объявленным объектам в пределах одной области
int x = 1;
int x = 2;
int x = 3;

print(x); // 3
print(../x); // 2
print(../../x); // 1
Обращение через внешнюю область
Существует и другой механизм, он позволяет работать с перекрытыми полями при наследовании. Идея в том, чтобы обращение происходило не напрямую, а через имя соответствующего класса (см. Пример кода №10). Такой подход достаточно распространен и его можно было бы развить. Например, аргументы функции могут быть получены через одноименный префикс (см. Пример кода №11).
Пример кода №10
Обращение к перекрытым полям в С++
struct A
{
int x = 1;
};

struct B: public A
{
int x = 2;
void F()
{
print(A::x); // 1
print(x); // 2
}
};
Пример кода №11
Обращение к перекрытым аргументам функции
void f(int x = 1)
{
int x = 2;
print(x); // 2
print(f.x); // 1
}
Такой же подход можно применить к циклам если разрешить их именование (см. Пример кода №12). Стоит отдельно подчеркнуть то, что это так же решило бы проблему с выходом из нескольких циклов (см. Пример кода №13), поскольку команда break так же является заложником механизма перекрытия.
Пример кода №12
Обращение к перекрытой переменной цикла
for row (int index = 0; index < 10; ++index)
{
for column (int index = 0; index < 10; ++index)
{
print( matrix[row.index][column.index] );
}
}
Пример кода №13
Обращение к инструкции break внешнего цикла
for outer (int i = 0; i < 10; ++i)
{
for inner (int j = 0; j < 10; ++j)
{
outer.break; // вихід з зовнішнього циклу
}
}
Выводы
Существующая реализация перекрытия ограничена и лишена многих возможностей. Но ее можно развить, особенно принимая во внимание существующие решения схожих проблем. Предложенные в этой статье подходы интуитивно понятны и относительно просты в реализации. В зависимости от дизайна языка их можно использовать не только по отдельности, но и комбинировано.
1
В этой статье я буду использовать термин перекрытие вместо привычного сокрытия.
2
Именно по этому я и называю такое перекрытие необратимым.
3
Характерно то, что судя по всему авторы явно знали что делают. При попытке перекрыть существующий объект выводиться предупреждение, что это может привести к неоднозначности.
4
В крайнем случае это можно назвать дополнительными возможностями.
5
Стоит заметить и отсутствие единого подхода к решению проблемы. В отличии от часто используемых средств, типа обращения к полям объекта через точку или вызовам функций через круглые скобки, здесь нет общепринятого стандарта.
6
Это работает только для упорядоченных областей, то типу блока или тела функции. Для пространств имен или классов этот подход не применим.