Замыкания по значениям против контекста - PullRequest
3 голосов
/ 09 января 2012

Я думаю о различных реализациях замыканий и задаюсь вопросом о достоинствах разных стилей. Кажется, есть два варианта, закрывающих контекст выполнения или значения. Например, в контексте мы имеем:

a = 1
def f():
  return a
f() # returns 1
a = 2
f() # returns 2

В качестве альтернативы, мы можем закрыть значения и иметь:

a = 1
def f():
  return a
f() # returns 1
a = 2
f() # returns 1

Существуют ли языки, которые реализуют второй? Есть ли преимущества и недостатки?

Ответы [ 6 ]

2 голосов
/ 10 января 2012

Я думаю, что в этом случае дело не в контексте и значении, а в том, закрываете ли вы переменную в качестве ссылочной ячейки или значение, которое содержит переменная.

Если вы действительно имеете в виду контекст,Вы имеете в виду динамический или лексический контекст.См. эту статью в Википедии для подробного сравнения.

Большинство языков реализуют лексическую область видимости (или пытаются это сделать).Некоторые языки реализуют динамическую область видимости: в частности, более старые Lisps, такие как ELisp для emacs.Большинство языков с замыканиями (например, Scheme, Haskell, ML и т. Д.) Закрывают значения в лексической области видимости.Динамический охват часто считается плохой идеей, потому что его сложнее рассуждать (это «жуткое действие на расстоянии»).

Обратите внимание, что даже в лексически ограниченных языках вы можете получить поведение, подобное первому примеру, если вызакройте ссылку на ячейкуВот почему замыкания Scheme и JavaScript ведут себя так же, как и они (поскольку переменные являются ссылочными ячейками).

1 голос
/ 10 января 2012

У разных языков есть один из этих двух способов или оба.

Основное различие заключается в том, что происходит, когда вы присваиваете переменную. Таким образом, как указывали другие, в языках, где переменные неизменны

В языках, которые фиксируются по значению, одна проблема заключается в том, как обрабатывать присвоения этой переменной. Так как он захвачен значением

  • Как отмечали другие, многие языки без явного синтаксиса для работы с захватом по значению или захватом ссылки по ссылке, включая: Python, Ruby, JavaScript, Scheme, Perl, Go, Smalltalk и т. Д.
  • Как уже указывали другие, можно сказать, что языки ML (SML, OCaml) и Haskell захватываются по значению, поскольку их переменные неизменны, поэтому между ними нет реальной разницы, а захват по значению проще
  • Как уже отмечали другие, Java требует, чтобы захваченные переменные были final, в основном для целей захвата по значению, потому что в противном случае возникло бы путаница из-за наличия двух отдельных изменяемых копий переменной в одной и той же области видимости; но когда они final, они не могут быть изменены, поэтому нет разницы между наличием одной копии и нескольких копий
  • C ++ 11 позволяет выбрать, захватывать ли по значению или по ссылке. Вы перечисляете переменные для захвата в скобках. Переменные с & указаны по ссылке; в противном случае это по значению. = сам по себе захватывает все незарегистрированные переменные по значению; & сам по себе захватывает все незарегистрированные переменные по ссылке. Нужно быть осторожным при захвате переменных по ссылке, чтобы не захватывать переменные, которые выходят за рамки. Интересно, что (в отличие от Java) можно захватывать переменную по значению, но иметь значение mutable , используя модификатор mutable для анонимной функции.
  • PHP также позволяет вам выбирать, когда вы объявляете переменные для захвата. & обозначает захват по ссылке; в противном случае по значению.
  • Блоки в инструментах разработки Apple (для языков C, C ++ и Objective-C; доступны в Mac OS X 10.6+ и iOS 4+) также позволяют выбирать. Когда вы впервые создаете блок, он имеет доступ к захваченным переменным по ссылке; однако такому блоку не разрешается покидать область действия (например, возвращаться), если он захватывает локальные переменные, поскольку они выходят за пределы области действия. Нужно скопировать блок, чтобы он вышел из области видимости; захваченные переменные захватываются по значению при копировании блока. Также возможно указать, что локальная переменная должна быть захвачена ссылками блоками при копировании с помощью модификатора __block при объявлении этой переменной. Это, вероятно, выделяет его в куче.
1 голос
/ 10 января 2012

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

Это не так, потому что оптимизатор может заменить переменную на ее значение, в частности, если переменная объявлена ​​как:

val x = 1;

оно принимается за неизменное значение, и такая замена считается безопасной. Это верно, даже если значение передается в качестве аргумента! Например:

fun f(x:int) () => x;
val y = 1;
val fy = f y;  // closure formed
println$ fy();

Скорее всего, мы определили как:

val fy = fun () => 1;

было написано. В этом случае это может быть то же самое для переменной:

var z = 1;
val fz = f z;
z = 2;
println$ fz (); // prints 1 .. maybe

, заменив x значением z во время формирования замыкания, НО также может вывести 2, заменив x вместо имени переменной z.

В Феликсе не определяет , какая оптимизация применяется, и это преднамеренно: это дает компилятору свободу выбора (что он считает) наилучшей оптимизации.

Если вы хотите форсировать интерпретацию, вы можете: для аргумента параметра:

fun f (var x: int) () => x; // форсирует энергичную оценку, копирует аргумент в параметр fun f (x: unit -> int) => x (); // заставляет ленивую оценку

И для первоначального вопроса: вы можете вызвать ленивую интерпретацию, просто используя указатель:

var x = 1;
fun f()=> *&x;

Глупо толкать энергичную интерпретацию. Если вы хотите, чтобы вы сделали это:

var x = 1;
val y = x;
var x = 2;
fun f() => y; // prints 1

Я должен сказать, что я НЕ СЧАСТЛИВ с этой семантикой, но это то, что происходит в данный момент, и это кажется вполне логичным. Что более тревожно, так это:

var g : unit -> int;

for var i = 0 upto 10 do
   val x = i;
   fun f()() => x;
   if i == 3 do
     g = f();
   done
done

Цикл for плоский, без стековой рамки. Здесь «х» - это значение, но оно не является неизменным! Если вы можете предсказать значение, напечатанное с помощью g (), то вы справляетесь лучше меня (и я разработал язык:)

К сожалению, оптимизация, полученная с помощью этой семантики, является обязательной: мы не хотим в конечном итоге получить производительность, ну, ну, в общем, Хаскелла (без обид).

Мораль этой истории такова: если ваш код зависит от ответа на вопрос ОП, от вашей головы это будет! Напишите код, в котором семантика определена, если вам требуется .

1 голос
/ 10 января 2012

Замыкания должны вести себя как в первом случае, но некоторые языки предоставляют второй случай.

Smalltalk работает по первому случаю. Давайте предположим, что класс определяет методы m и test :

m
| counter c |  "temporary vars"
counter = 0.
c = [ counter = counter + 1. counter ]. 
^ c. "returns the closure"

test
| c | "temporary vars"
c = self m. "obtain a closure that increments a counter"
c value. "return 1"
c value. " returns 2"

Чтобы думать о закрытии, вы должны думать о стеке. Если замыкание c определено в методе m и закрывается по временной переменной counter , кадр стека m не может быть удален, пока закрытие мусора. Закрытие первоклассное, поэтому вы не знаете, когда на него больше не будет ссылок.

Но многие замыкания не закрываются по какой-либо временной переменной и не закрываются по временным переменным, которые не были изменены после определения замыкания. В последнем случае значение временной переменной в момент определения замыкания можно скопировать в замыкание, чтобы им не требовалась ссылка на кадр стека m .

В случае закрытия c выше, закрытие может копировать значение counter . Это то, что Java предписывает, заставляя временные переменные, которые закрыты, быть окончательными.

Если метод м был

m
| counter c |  "temporary vars"
counter = 0.
c = [ counter = counter + 1. counter ].
counter = 1. 
^ c. "returns the closure"

Я полагаю, что это победит оптимизацию, потому что counter мутирует после создания замыкания.

Вот так я понимаю замыкания, по крайней мере.

1 голос
/ 10 января 2012

В большинстве языков как с замыканиями, так и с изменяемыми переменными замыкания фиксируют местоположения, а не значения (то есть первое поведение).Примеры включают в себя Scheme, Python и Javascript.

Чтобы сделать это безопасно, во многих случаях язык должен выделять изменяемые переменные, которые захватываются замыканиями.Обычно это реализуется с помощью прохода компилятора, который преобразует переменные, которые фактически преобразованы в явно выделенные изменяемые ячейки, после чего компилятор может забыть о проблеме.

Чтобы избежать неявного выделения кучи, Java требует (требуется?) захваченные переменные (внутренними классами), которые должны быть объявлены i fnal (т.е. неизменяемыми).Другие языки, такие как ML и Haskell, полностью избегают этой проблемы, потому что переменные всегда неизменны.В C ++ захват по ссылке может быть небезопасным, как указывает Джон в своем ответе.

1 голос
/ 09 января 2012

C ++ лямбда-выражений можно записать явно по значению:

int a = 1;
auto f1 = [a]() -> int { return a; }
f1() == 1;
a = 2;
f1() == 1;

или по ссылке:

a = 1;
auto f2 = [&a]() -> int { return a; }
f2() == 1;
a = 2;
f2() == 2;

Вы также можете неявно захватить любой из способов:

auto f1 = [=]() -> int { return a; }
auto f2 = [&]() -> int { return a; }

Преимущество заключается в том, что вы контролируете, какие переменные копируются или на которые ссылаются.Потенциальный недостаток заключается в том, что вы должны остерегаться проблем, связанных со временем жизни, поскольку ссылки на C ++ не принадлежат: если a выходит из области видимости, то вызов f1 все еще действителен, но вызов f2 не определен.Если это естественно, и вы не возражаете против накладных расходов, вы всегда можете захватить shared_ptr<T> (указатель с общим владением).

Так же для неизменяемых значений:

  • Захват по значению заставляет копию.Захват по ссылке не выполняется.

  • Захват по стоимости не имеет проблем с владением.Захват по ссылке делает.

Для изменяемых значений вы, конечно, должны захватывать по ссылке.Вот надуманный пример, похожий на std::partial_sum():

int sum = 0;
auto f = [&sum](int i) -> int { sum += i; return sum; }

vector<int> input{1, 2, 3, 4, 5};
vector<int> output;
transform(begin(input), end(input), back_inserter(output), f);

sum == 15;
output == vector{1, 3, 6, 10, 15};
...