Как использовать чистую в D 2.0
Во время игры с D 2.0 я обнаружил следующую проблему:
Пример 1:
pure string[] run1()
{
string[] msg;
msg ~= "Test";
msg ~= "this.";
return msg;
}
Это компилируется и работает как ожидалось.
Когда я пытаюсь обернуть массив строк в классе, я не могу заставить это работать:
class TestPure
{
string[] msg;
void addMsg( string s )
{
msg ~= s;
}
};
pure TestPure run2()
{
TestPure t = new TestPure();
t.addMsg("Test");
t.addMsg("this.");
return t;
}
Этот код не будет компилироваться, потому что функция addMsg нечиста. Я не могу сделать эту функцию чистой, поскольку она изменяет объект TestPure. Я что-то пропустил? Или это ограничение?
Следующее компилирует:
pure TestPure run3()
{
TestPure t = new TestPure();
t.msg ~= "Test";
t.msg ~= "this.";
return t;
}
Не будет ли оператор ~= реализован как нечистая функция массива msg? Почему же компилятор не жалуется на это в функции run1?
5 ответов
Начиная с v2.050, D смягчил определение pure
принять так называемые "слабо чистые" функции тоже. Это относится к функциям, которые " не читают и не пишут какие-либо глобальные изменяемые состояния". Слабо чистые функции - это не то же самое, что чистые функции в смысле функционального языка. Единственное отношение состоит в том, что они создают действительно чистые функции, то есть "сильно чистые" функции, способные вызывать слабые, как в примере с OP.
С этим, addMsg
можно пометить как (слабо) pure
, поскольку только локальная переменная this.msg
изменено:
class TestPure
{
string[] msg;
pure void addMsg( string s )
{
msg ~= s;
}
};
и, конечно, теперь вы можете использовать (сильно) pure
функция run2
без изменений.
pure TestPure run2()
{
TestPure t = new TestPure();
t.addMsg("Test");
t.addMsg("this.");
return t;
}
Другие уже указывали, что addMsg не является чистым и не может быть чистым, потому что он изменяет состояние объекта.
Единственный способ сделать его чистым - это внести изменения, которые вы делаете. Самый простой способ сделать это с помощью обратной мутации, и для этого есть два способа.
Во-первых, вы можете сделать это так:
class TestPure
{
string[] msg;
pure TestPure addMsg(string s)
{
auto r = new TestPure;
r.msg = this.msg.dup;
r.msg ~= s;
return r;
}
}
Вам нужно скопировать предыдущий массив, потому что внутри чистой функции ссылка this на самом деле является константой. Обратите внимание, что вы могли бы сделать копию лучше, выделив новый массив окончательного размера, а затем скопировав элементы в себя. Вы бы использовали эту функцию так:
pure TestPure run3()
{
auto t = new TestPure;
t = t.addMsg("Test");
t = t.addMsg("this.");
return t;
}
Таким образом, мутация ограничивается каждой чистой функцией с изменениями, передаваемыми через возвращаемые значения.
Альтернативный способ написания TestPure состоит в том, чтобы сделать члены константными и выполнить всю мутацию, прежде чем передать ее конструктору:
class TestPure
{
const(string[]) msg;
this()
{
msg = null;
}
this(const(string[]) msg)
{
this.msg = msg;
}
pure TestPure addMsg(string s)
{
return new TestPure(this.msg ~ s);
}
}
Надеюсь, это поможет.
Пожалуйста, просмотрите определение чистых функций:
Чистые функции - это функции, которые выдают одинаковый результат для одних и тех же аргументов. Для этого чисто функция:
- имеет параметры, которые все являются инвариантными или неявно преобразуются в инвариантные
- не читает и не записывает глобальное изменяемое состояние
Одним из результатов использования чистых функций является то, что они могут быть безопасно распараллелены. Однако небезопасно выполнять несколько экземпляров вашей функции параллельно, поскольку они оба могут одновременно изменять экземпляр класса, вызывая проблему синхронизации.
Я думаю, что ваш код концептуально правильный. Однако вы, возможно, нашли случай, когда семантический анализ компилятора не так хорош, как ваш мозг.
Рассмотрим случай, когда источник класса недоступен. В этом случае компилятор не сможет сказать, что addMsg
только изменяет переменную-член, поэтому она не может позволить вам вызывать ее из чистой функции.
Чтобы разрешить это в вашем случае, он должен иметь особую обработку для этого типа использования. Каждое добавленное правило особого случая делает язык более сложным (или, если оставить его недокументированным, делает его менее переносимым)
Просто догадка, но эта функция не всегда возвращает один и тот же результат.
Видите, он возвращает ссылку на некоторый объект, и хотя объект всегда будет содержать одни и те же данные, объекты, возвращаемые несколькими вызовами одних и тех же функций, не идентичны; то есть они не имеют один и тот же адрес памяти.
Когда вы возвращаете ссылку на объект, вы по существу возвращаете адрес памяти, который будет отличаться при нескольких вызовах.
Другой способ думать об этом, часть возвращаемого значения - это адрес памяти объекта, который зависит от некоторого глобального состояния (состояний), и если выходные данные функции зависят от глобального состояния, то это не чисто. Черт, это даже не должно зависеть от этого; пока функция читает глобальное состояние, она не является чистой. Называя "новый", вы читаете глобальное состояние.