LINUX.ORG.RU

Не понимаю эти ваши замыкания в Go

 , , ,


0

2

Читаю урок по анонимным функциям:


Преимуществом анонимных функций является то, что они имеют доступ к окружению, в котором они определяются. Здесь функция square определяет локальную переменную x и возвращает анонимную функцию. Анонимная функция увеличивает значение переменной x и возвращает ее квадрат. Таким образом, мы можем зафиксировать у внешней функции square состояние в виде переменной x, которое будет изменяться в анонимной функции.


Хорошо, то есть, анонимная функция будет иметь какой-то хитрый доступ к переменной x, существующей в функции square(). Надо это проверить, и вызвать square() между вызовами анонимной функции.

И что можно увидеть?

Код:

package main
import "fmt"

func square() func() int {
	var x int = 2
	fmt.Println("X variable in square function ", x)

	return func() int {
		x++
		return x * x
	}
}

func main() {
	f := square()
	fmt.Println(f())
	fmt.Println(f())

	square() // <---------- Проверка

	fmt.Println(f())
	fmt.Println(f())
}

Результат:
X variable in square function  2
9
16
X variable in square function  2
25
36

Получается, что анонимная функция не работает с переменной x, размещенной в square(). Происходит какое-то копирование переменной, и потом работа идет с этой копией. То есть, реальность для переменной x «расщепляется»: Внутри square() у нее одно значение, а при вызове анонимной функции - другое.

Как это можно понять? Как это можно уложить в голове? Как этим можно пользоваться?

★★★★★

Последнее исправление: maxcom (всего исправлений: 1)

Может пример на питоне поможет понять замыкания и анонимные функции:

def power(x):
    return lambda y: y ** x

square = power(2)
square(8)  # 64

Upd. Так тоже можно, но менее понятно и менее похоже на ваш пример:

power = lambda x: lambda y: y ** x
vvn_black ★★★★★
()
Последнее исправление: vvn_black (всего исправлений: 1)

var x на каждый вызов square() свой, уникальный, вот если его сделать static, если так в Go можно, то он будет один.

Он не размещен в square(), а создается при выполнении функции.

MOPKOBKA ★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 1)

Ты вызвал square(), в ней создалась переменная x и анонимная функция, которая получила ссылку на x. Функция вернула тебе ссылку на анонимную функцию. Дальше ты вызываешь её, тем самым инкрементируешь x.

Когда ты вызвал функцию square() ещё раз, она создала новую переменную x и новую анонимную функцию, но ты их не использовал, и их когда-нибудь уничтожит GC.

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

static_lab ★★★★★
()

анонимная функция не работает с переменной x, размещенной в square()

Функция, возвращённая из square, имеет доступ к x, определённому при этом конкретном вызове square. Функция, возвращённая при другом вызове square, будет иметь доступ к другому x. Замыкание — это функция плюс её лексическое окружение на момент создания функции.

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

Как это можно понять? Как это можно уложить в голове? Как этим можно пользоваться?

Да легко, на самом деле. Вот сейчас почитаешь тред, просветлишься и дальше как по маслу пойдёт %)

Nervous ★★★★★
()

Когда вызываешь square, считай, создаёшь объект с приватным полем x и единственным методом, который инкрементит x и возвращает новое значение, возведённое в квадрат. Если бы твоя анонимная функция принимала аргумент с именем «сообщения», то ты мог бы организовать систему вызова произвольного «метода».

Лучше всего найди время и прочитай SICP, чем слушать, как мы тут на пальцах объясняем.

buddhist ★★★★★
()
Последнее исправление: buddhist (всего исправлений: 1)

Возможно, так будет чуть понятней:

func main() {
	f := square()

	fmt.Printf("f() = %v\n", f())
	fmt.Printf("f() = %v\n", f())

	g := square() // <---------- Проверка

        fmt.Printf("f() = %v\n", f())
	fmt.Printf("f() = %v\n", f())

        fmt.Printf("g() = %v\n", g())
	fmt.Printf("g() = %v\n", g())
} 
theNamelessOne ★★★★★
()

Как это можно понять? Как это можно уложить в голове? Как этим можно пользоваться?

Как говорят, «замыкания это объекты для бедных». Так что считай что своим вызовом square() ты создаешь новый объект со своим внутренним состоянием (это своего рода конструктор), а потом вызываешь его единственный метод, который имеет доступ к этому состоянию.

maxcom ★★★★★
()
Последнее исправление: maxcom (всего исправлений: 1)

Получается, что анонимная функция не работает с переменной x, размещенной в square(). Происходит какое-то копирование переменной, и потом работа идет с этой копией. То есть, реальность для переменной x «расщепляется»: Внутри square() у нее одно значение, а при вызове анонимной функции - другое.

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

Как это можно понять? Как это можно уложить в голове? Как этим можно пользоваться?

Представь, что square() это у тебя не функция, а код самого верхнего уровня. Тогда, получается, что переменная x это самая обычная переменная а замыкание самая обычная функция (только без имени).

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

Во демка, которая это все наглядно показывает. Постарайся представить себе foo() как отдельную программу со своим окружением (глобальными переменными). Ну а разница в том, что в реальности у тебя матрешка из таких окружений: глобальное в программе, в него вложенное окружение внутри функции, и т. д.
https://go.dev/play/p/uhrDlr41FLH

urxvt ★★★★★
()
Ответ на: комментарий от urxvt

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

Нет.

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

Везде встречаю именно эту формулировку, но она ничего не объясняет а только запутывает еще больше.

В этой фразе нигде не сказано, что при каждом вызове square() переменная x создается. А значит, при первом вызове создастся x в одном месте памяти, при втором вызове - в другом месте памяти.

При обычной работе функции square() переменная x будет уничтожена в конце работы функции. А если произойдет захват этой переменной через замыкание - то будет удалена когда прекратит существование само замыкание.

В любом случае, повторный вызов square() генерирует повторное создание переменной, и получить доступ к предыдущей переменной невозможно (если не оформить переменную как static или как там это делается в Goшечке).

Xintrea ★★★★★
() автор топика
Последнее исправление: Xintrea (всего исправлений: 2)
Ответ на: комментарий от Xintrea

В этой фразе нигде не сказано, что при каждом вызове square() переменная x создается. А значит, при первом вызове создастся x в одном месте памяти, при втором вызове - в другом месте памяти.

Все верно. Но то что она создается каждый раз при вызове square() следует из то, что при каждом вызове square() создается новый экземпляр контекста и новое замыкание. Оба вызова никак между собой не связанны.

f1 := square()
f2 := square()

Смысл в том, что f1 и f2 это два разных замыкания и, соответственно, они захватили два разных контекста в которых два разных x.

если не оформить переменную как static или как там это делается в Goшечке

Го такого не поддерживает. Нужно использовать глобальную переменную в таком случае.

urxvt ★★★★★
()

Как этим можно пользоваться?

Зачем ООП существует представляете? Вот тут то же самое, замыкание объединяет некие данные и поведение вместе.

ugoday ★★★★★
()
Ответ на: комментарий от komeiji

Rust, кстати, намекает на то что это значение будет переиспользовано:

fn square() -> impl FnMut() -> i32 {
    let mut x = 2;
    println!("X variable in square function {x}");
    move || {x += 1; x * x}
}

fn main() {
    let mut f = square();

    println!("{}", f());
    println!("{}", f());

    let _ = square();

    println!("{}", f());
    println!("{}", f());
}
cumvillain
()
Ответ на: комментарий от Nervous

Смотришь на код - и всё сразу понятно. Я вообще алгоритмы иногда блок-схемами набрасываю и максимально дроблю код на функции.

Shadow ★★★★★
()
Последнее исправление: Shadow (всего исправлений: 1)
Ответ на: комментарий от Shadow

Смотришь на код - и всё сразу понятно.

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

Nervous ★★★★★
()
Ответ на: комментарий от Nervous

Ну, для меня это (анонимные и лямбды) выглядит, как будто все участники пьесы играют её, построившись в гимнастическую пирамиду, а самый нижний ещё и на моноцикле едет.

Shadow ★★★★★
()
Последнее исправление: Shadow (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

C, Pascal, ... И прочие где нету gc или/и лямбд.

Но автора топика в первую очередь удивляет, что создаётся новая переменная для каждого вызова square(). И C, и Pascal в этом отношенни ведут себя так же, как и Go.

Waterlaz ★★★★★
()
Ответ на: комментарий от Nervous

Так и пиши, что не понимаешь, что только замыкания обладают истинной инкапсуляцией: данные внутри, и к ним снаружи не подобраться, если только это не предусмотрено самим замыканием через получение аргументов изве. А данные в объектах это как девушка, у которой известное место что фиговым листком прикрывай, что пояс верности вешай - лишь вопрос упорства туда попасть ;))

anonymous
()
Ответ на: комментарий от anonymous

Так и пиши, что не понимаешь

Две вещи я не понимаю: первое — отчего горят на небе часты звёздочки, а другое — отчего я такой добрый при моей-то тяжёлой жызни (тм).

Nervous ★★★★★
()
#include <iostream>

class square {
    int x = 2;
public:
    square() {
        std::cout << "X variable in square functor " << x << std::endl;
    }
    int operator()() {
        x++;
        return x * x;
    }
};

int main() {
    auto f = square();
    std::cout << f() << std::endl;
    std::cout << f() << std::endl;
    
    square();

    std::cout << f() << std::endl;
    std::cout << f() << std::endl;

    return 0;
}
fluorite ★★★★★
()
Ответ на: комментарий от flant

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

а так… замыкание - довольно бестолковое понятие, ничто критическое на него не завязано. просто это кровавое наследие функциональщины.

alysnix ★★★
()
Ответ на: комментарий от vvn_black

Это не объекты, но при этом внутреннее состояние сохраняется, и данные связываются с функцией, которая работает с ними.

Тут есть тонкий момент. Непонятно, функция замыкает только те данные,с которыми работает или нет? То есть, если в внешнем скопе относительно функции были и другие переменные, они замкнутся?

А что будет, если при повторных вызовах замкнутой функции, по каким-то условиям будет написан доступ к переменной, которая не была замкнута при первом вызове?

UPD: Я сейчас проверил - как минимум, те переменные, которые присутствуют в замыкании, замыкаются. Даже если при первом вызове к ним нет обращения.

Теперь становится непонятно: замыкается весь скоп, или только те переменные, которые упоминаются в замыкании?

Xintrea ★★★★★
() автор топика