Pull to refresh

Comments 62

Интересно почему изначально была выбрана видимость per-loop, а не per-iteration? Для экономии памяти?

наверно, просто не продумали этот момент, сделали как проще

Вы прям идеально описали мои мысли, которые то тут то там возникали при освоении Go. Простой как валенок, в этом его прелесть, но к некоторым моментам было ощущение как вы описали.

Ну, не думаю, что на этом ресурсе найдётся кто-то, кто сможет их осудить. Все мы своего рода мэйнтэйнеры Go.

Вы случаем не путаете ментейнеров с евангелистами?

Традиция. Во многих языках есть (или было) такое же поведение: питон, ява, C#, javascript. По-отдельности реализация циклов и лямбд выглядит естественной, но их взаимодействие даёт неочевидное последствие.

ява

Будьте любезны, пример. Что-то придумать не могу, учитывая что в замыкание в Java можно захватить только final или effectively final переменную.

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

Именно так, а go в мусорное ведро

В java не так (там нельзя передавать не effectively final). В javascript не так (всё нормально захватывается). В python свободная переменная передаётся по имени, это вообще что-то из 60-х и алгола, но это понятное поведение.

c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.

Дурость с for я видел только в Go. К сожалению, язык миновал стадию проектирования и сразу ушёл в продакшен.

В javascript не так (всё нормально захватывается).

Вот прямо сейчас запустил в браузере:

var funcs = [];
for (var i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log("My value:", i);
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

Результат:

My value: 3
My value: 3
My value: 3

c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.

Было как в javascript, потом пофиксили (нет, не так как сделано в java, где запрещено захватывать переменную цикла). Тут в комментариях уже про это писали.

Вы в курсе что с 2014 года уже не используется var? Как минимум в хроме, остальные подтянулись к 2016. То есть минимум 7 лет как проблемы нет.

Да. Можно сказать, что он для этого и создавался - чтобы у переменной была понятная и ожидаемая (с точки зрения обычного человека) область видимости.

Вы в курсе что с 2014 года уже не используется var

Цитирую себя: "Во многих языках есть (или было) такое же поведение"

В js, оказывается, и есть и было. Хорошо, буду знать, спасибо.

Просто добавлю, что let был в фаерфоксе со времён первой браузерной. Но сначала его принципиально не хотел понимать ИЕ, потом хром. Потом разработчики хрома таки что-то поняли.

Ну, разработчики IE тоже поняли, только поздновато — в 11-й версии ;-) И то не во всех режимах.
Была у меня с IE такая вот хохма во дни минувшие. Делаю модуль расширения для ADFS на Win2012 R2. Ему там положено возвращать фрагменты HTML, которые ADFS вставляет в свой шаблон и возвращает получившуюся страницу пользователю. Проверяю работу в IE11- let в скриптах в фрагментах не работает. А те же самые фрагменты, вставленные в статический файл HTML — копию возвращаемой страницы — работают на ура. Сперва поофигевал, потом разобрался: AD FS передавал в IE заголовок, включающий режим совместимости с IE10 — а в том режиме let предусмотрен не был.

Хорошо когда клиенты обновляют свое ПО и компьютеры.
У меня 20% клиентов использует IE

В С++ никаких новых переменных на каждой итерации не создаётся. Но там захватывать переменную цикла по ссылке - уже взвод курка для выстрела в ногу.

В плюсах такой проблемы нет, из-за другой модели работы с памятью. Если мы захватим переменную по указателю то по завершении цикла мы получим dangling pointer. В го же из-за автоматического управления памятью так сделать можно без того чтобы получить ошибку доступа к памяти.

Теоретически можно захватить счётчик по ссылке на одной итерации и использовать полученную лямбду на другой - это и в С++ валидно (и как раз такой код будет ломаться от описанного изменения в Go). Другое дело, что большого практического смысла нет.

в питоне такое же поведение:

In [1]: func = [lambda: i for i in range(5)]

In [2]: func[0]()
Out[2]: 4

В питоне-то понятно, почему: тело функции (в данном случае лямбды) не интерпретируется до момента её вызова. Там любой лексически верный мусор можно написать между : и for.

>>> func = [lambda: j for i in range(5)]
>>> j = 'Hello'
>>> func[0]()
'Hello'

А что вы хотели показать этим примером? Это вполне ожидаемое поведение, если нет локальной переменной с таким именем, то питон будет искать среди глобальных переменных (вернее Enclosing, Global, Built-in). Ну и при этом на момент объявления функции эта переменная существовать не обязана.

In [1]: def f():
   ...:     print(j)
   ...:
In [2]: j = "Hello"
In [3]: f()
Hello

Ну да, так всё и есть.

Я хотел показать именно то, что странно было бы ожидать, что значение i будет меняться в элементах списка func на каждой итерации, если оно фактически востребуется только один раз в момент вызова функции в последней строке.

Для такого поведения, как в новом Go, в питоне нужно было б заводить отдельный контекст для каждой итерации цикла, что обессмыслило бы само понятие перементой цикла.

можно вот так сделать:

In [1]: [f() for f in map(lambda i: lambda: i, range(5))]
Out[1]: [0, 1, 2, 3, 4]

UFO just landed and posted this here

c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.

Там loop-level в for, а вот в foreach зависит от версии языка: изначально было loop-level, но в C# 5 поменяли на iteration-level.

c# проверить не могу (и не хочу)

Об этом было очень важно упомянуть.

Посчитал, что важно. Чтобы отмести предложения сделать это разными способами.

В Python у циклов (как и у if) вообще нет своей области видимости, можно переменную вообще первый раз внутри цикла присвоить, и снаружи потом использовать. Если не присвоишь - просто будет NameError

>>> for i in range(10):
...   a = 2
... 
>>> print(i)
9
>>> print(a)
2
>>> print(c)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined

Вы еще спросите, почему в JS видимость this везде разная.
Просто провтыкали, как обычно.

Прощай старый добрый способ докопаться на собесах)

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

На собесах хорошо отлавливает джунов

Если человек помнит или хотя бы подзабыл, перед вами джун, если прокричал что-то на орочьем, достал топор и сближается, значит про опыт не врет

По-моему такой вопрос реально отражает опыт, я сам с этим поведением for ... range cтолкнулся.
Меня больше смущают вопросы про поведение, если писать (или читать) в закрытый канал (буфферизованный), если так приходится делать - это уже плохой код. Зачем даже задуматься о том, какое будет поведение?

Минус один вопрос на собесах и в тестах! ха-ха(

Зато +1 вопрос — breaking changes между go 1.21 и 1.22 :)

В С# сделали то же самое в 2012 году - полет нормальный, никто не жалуется.

на самом деле мне интересно, как это будет сделано.

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

а будет как? на каждую итерацию будет новая переменная?

звучит очень дорого, а если в цикле миллион итераций?

Звучит бесплатно, если речь о примитивах.

Ссылка на примитив как и любая другая ссылка весит 32 или 64 бита в зависимости от разрядности. Значение int весит 32 бита. В замыкание неизбежно что-то копировать да придётся - либо ссылку, либо значение. Копировать значение примитива стоит не дороже ссылки, а иногда дешевле (если у нас 64 битная ОС, а примитив 32 бита). Также чтение примитива по значению точно быстрее, чем по ссылке, потому что примитив читается за одну операцию, а примитив за ссылкой за две (сначала прочитать ссылку, потом значение по адресу из неё). Наконец, оптимизатор, зная что примитив никто за пределами лямбды не изменит, может лучше оптимизировать код.

В языках типа C++, где ссылки создаются более явно, давно есть правило, что примитивы по ссылке передают только если очень надо. По значению эффективнее. По ссылкам хорошие программисты передают объекты, которые занимают в памяти больше размера 2-3 указателей (никакие примитивы столько не весят), либо если нужны особые свойства ссылок (возможность менять из нескольких мест и т. д.)

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

Новость об изменении этого поведения как раз. А мой комментарий о том, что для примитивов это повысит эффективность, а не понизит. Если примитивы в Go как в Java (выделяются не в куче и не имеют таблицы виртуальных методов), а не как в Python.

Новость об изменении этого поведения как раз

Новость о том, что на разных итерациях разные переменные. Но внутри каждой итерации переменная одна, никто не мешает захватить её в теле цикла несколько раз в разные closure.

да не копируем итератор итератор в отдельную переменную на каждой итерации (и замыкаемся по ссылке на копию).
Если копией не воспользовались - всё отлично DCE её легко удалит.

Думаю, что если нет захвата переменной в замыкание, то можно и не создавать лишнего.

А как сейчас в Го продляется жизнь замкнутых переменных со стека?
Через двойной указатель и копирование в кучу при выходе из скоупа?

Ну и теоретически можно предусмотреть отдельную машинерию только если замыкание захватывает переменную цикла.

Upd: если в го есть понятие "объект расположенный на стеке" (с конструкторами \ деструкторами которые компилятор умеет элиминировать если они пустые) - то даже специальный случай вроде не потребуется.

Она не продляется, для этого есть escape analysis - если он говорит что значение переживает функцию оно сразу аллоцируется в куче

Спасибо.

Тогда моё update наверное не верно. Если у примитивных типов нет технического деструктора (вызываемого language-runtime, в абсолютном большинстве случаев не вызываемого), не на что навесить нужную логику (вводить его сейчас, понятно, поздновато).


И значит скорее всего надо отдельно делать циклы без замыканий (ничего не менять) \ отдельно циклы где замыкания захватывают переменную цикла.

Копируете переменную цикла. Замыкаетесь по копии переменной. Удаляете ненужные скопированные переменные (на стеке DCE справится, а вот если сделали выделение памяти - надо уже ручками удалять).

ПС
Желательно эти 3 фазы поставить подряд.

Если анализ гитхаба и другого опенсорса говорит, что ничего не сломается от такого изменения, то ничего страшного.

Ломающее изменение и подъем только минорной версии языка? Что-то я не понимаю в semver

"Если раньше в циклах были проблемы с замыканиями, так как переменная цикла имела скоуп".
...
Им это слово много говорило. Жаргон это конечно хорошо, но все же...
Даже ПЕРВЫЙ ЖЕ коммент использут нормальное слово "видимость".

Тут в соседней статье пишут "холодный аутрич"

скоуп уже давно общепринятый термин. Ну и кстати...

жаргон - это французское слово
коммент - это английское слово

Троллинг засчитан. ;)
"скоуп уже давно общепринятый термин". Сорри (анлийское слово), но нет.

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

Если бы можно было всем договорится о "правильном" переводе, я бы выбрал "ареал".

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

О, я с таким сталкивался, совершенно не ожидая такого поведения. Подумал "фу" и обернул тело цикла в вызов функции, совсем как в js когда-то

Sign up to leave a comment.

Articles