Документация Engee
Notebook

Замыкания в программировании.

Замыкание (closure) — это функция, которая сохраняет доступ к переменным из своей лексической области видимости даже после того, как внешняя функция завершила выполнение. Это мощная концепция в программировании, особенно в языках с поддержкой функционального программирования.

В представленном примере на Julia мы видим классическую реализацию замыкания:

In [ ]:
function make_counter()
    count = 0  # Эта переменная «замыкается» внутри функции
    function counter()
        count += 1
        return count
    end
    return counter
end
Out[0]:
make_counter (generic function with 1 method)

Здесь make_counter является фабричной функцией, которая создает и возвращает функцию counter. Внутренняя функция counter имеет доступ к переменной count даже после того, как make_counter завершит свое выполнение.

Как работают замыкания

  1. Захват переменных: когда создается замыкание, оно «запоминает» все переменные из окружающей его области видимости.

  2. Состояние: эти переменные сохраняют свое состояние между вызовами замыкания.

  3. Изоляция: каждое новое замыкание создает свою собственную копию захваченных переменных.

В нашем примере каждый вызов make_counter() создает новое замыкание с собственной переменной count:

In [ ]:
my_counter = make_counter()  # Создаем первое замыкание
println("my_counter: $my_counter")
another_counter = make_counter()  # Создаем второе независимое замыкание
println("another_counter: $another_counter")
my_counter: counter
another_counter: counter

Для использования достаточно вызвать созданные счётчики.

In [ ]:
println("my_counter первый вызов: $(my_counter())")  
println("my_counter второй вызов: $(my_counter())")  
println("Вызов another_counter: $(another_counter())")  
my_counter первый вызов: 1
my_counter второй вызов: 2
Вызов another_counter: 1

Практические применения замыканий – это, например, реализация приватных переменных.

In [ ]:
function create_person(name)
    age = 0
    function get_name()
        return name
    end
    function get_age()
        return age
    end
    function have_birthday()
        age += 1
        return age
    end
    return (get_name=get_name, get_age=get_age, have_birthday=have_birthday)
end

person = create_person("Alice")
println(person.get_name())  # Alice
println(person.have_birthday())  # 1
Alice
1

Или мемоизация (кеширование результатов)

In [ ]:
function make_cached(f)
    cache = Dict()
    function cached(x)
        if !haskey(cache, x)
            cache[x] = f(x)
        end
        return cache[x]
    end
    return cached
end

# Пример использования
expensive_computation(x) = (sleep(1); x^2)
cached_comp = make_cached(expensive_computation)

println(cached_comp(2))  # Вычисляется (занимает время)
println(cached_comp(2))  # Берется из кеша (мгновенно)
4
4

Также возможно создание функций с предустановленными параметрами.

In [ ]:
function make_multiplier(factor)
    function multiplier(x)
        return x * factor
    end
    return multiplier
end

double = make_multiplier(2)
triple = make_multiplier(3)

println(double(5))  # 10
println(triple(5))  # 15
10
15

Вывод

Подводя итог, стоит разобрать плюсы и минусы такого подхода.
Преимущества:

  • инкапсуляция и скрытие данных,

  • создание функций с состоянием,

  • гибкость в создании специализированных функций,

  • поддержка функциональных паттернов программирования.

Недостатки:

  • потребление памяти (захваченные переменные не освобождаются),

  • возможные сложности с отладкой

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

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