Обзор функций замыкания и способов их использования

Введение

Если и есть что-то важное для каждого языка программирования, независимо от парадигмы этого языка, так это функции. Функции — это универсальная идея, переносимая из языка в язык, что имеет большой смысл. Даже когда мы занимаемся низкоуровневым программированием на ассемблере, мы по-прежнему используем то, что по сути является функциями. Хотя я не уверен, что они обязательно называются функциями, поскольку я всегда знал их как подпрограммы (или подпрограммы, если они запущены и не вызываются). Если вы не знаете, что я только что сказал, не волнуйтесь. …

К замыканиям отношения не имеет.

Моя точка зрения; функции универсальны в программировании. Одна из первых вещей, которую вы, вероятно, должны усвоить всякий раз, когда вы начинаете изучать новый язык программирования, — это то, как писать функции на этом языке. В некоторых случаях функции также могут быть невероятно гибкими, конечно, это зависит от языка. В некоторых случаях возможен определенный синтаксис, а в других нет. Одна удивительно мощная концепция в компьютерном программировании — это концепция замыкающих функций. Сегодня я хотел рассказать о том, что такое замыкающая функция и почему я считаю ее невероятно полезной техникой. Сегодня я буду использовать Julia, как и ожидали давние читатели, но функции замыкания вполне возможны во многих других языках, включая (но не ограничиваясь ими):

  • питон
  • C
  • C++

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



Пожалуйста, не смейтесь над моим отчаянием.

Что такое замыкающая функция?

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

«Замыкание — это связывание имен в лексической области с функциями первого класса».

Это описание действительно не поможет, если вы не понимаете, что означает лексическая область видимости или что такое первоклассные функции. К счастью, это всего лишь громкие названия простых концепций компьютерного программирования. Что ж, может быть, эти концепции не так просты для понимания изнутри, но их намного легче понять, глядя на код, чем на это описание. Лексическая область означает, что функция может получить доступ к области, которая традиционно ей недоступна. Рассмотрим типичный объем функции,

function example_of_scope(x, y)
    mu = mean([x, y])
    x += 5
end

Определение функции имеет частную область действия. Он может получить доступ к области над ним, в этом примере к глобальной области или, в Юлии, к «Главной». Это связано с тем, что, во-первых, это дочерний элемент этой области, а во-вторых, область действия глобального модуля Main всегда будет общедоступной. При этом, если мы затем переместимся ниже этой функции и попытаемся вызвать значения функции, мы получим ошибку:

function example_of_scope(x, y)
    mu = mean([x, y])
    x += 5
end

Область действия ограничена областью верхнего уровня, в данном случае модулем Main, вплоть до нашей функции, причем каждое подразделение содержит все больше и больше определений. Например, предположим, что у нас есть модуль под названием «Tacos» внутри Main, который содержит нашу функцию, тогда наша область видимости будет выглядеть так:

module Main
# |Main scope
  | module Tacos
   # Tacos scope
     |
      function example_of_scope(x,y)...
end

Это лексическая область видимости, поэтому в основном — большинство языков, которые вы, вероятно, использовали, используют лексическую область видимости, альтернативой является динамическая область видимости, что обычно означает, что все привязки имен являются глобальными. Мы можем получить доступ к определениям, определенным в Main и Tacos, из example_of_scope, но мы не можем получить доступ к определениям, определенным в example_of_scope(x, y), из Tacos или Main. Помните, что эти термины обычно относятся конкретно к функциям, и в большинстве случаев язык, который не имеет лексической области видимости, может раздражать при работе.

Это одна часть головоломки, так что же такое первоклассные функции? Функция первого класса — это просто функция, которую можно рассматривать как аргумент. В Julia мы можем передать тип ::Function и довольно легко принять функцию в качестве аргумента.

example(f::Function, x::Int64) = f(x)

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

function example2(n)
    h = function hello()
        blahblahblah
    end
end

Итак, теперь у нас есть область действия функции h, которая определена в области действия функции примера 2. Помните, важная часть функции замыкания, которая на самом деле называется функцией замыкания, заключается в том, что эта область видимости должна быть лексической. Это означает, что hello может получить доступ к области видимости example2. Все значения из указанной области сохраняются внутри определения функции. Например, мы можем получить доступ к n из функции hello.

function example2(n)
    h = function hello()
        println(n)
    end
    h
end
ourfunc = example2(5)
ourfunc()
5

Функции закрытия также были основой объектно-ориентированного синтаксиса внутри моего пакета OddFrames.jl, и если вы хотите просмотреть контекст, в котором они используются в этом пакете, вы можете посмотреть исходный код конструкторов. здесь:



Реализации на разных языках

Теперь, когда мы понимаем основы функций замыкания и их технические определения, давайте рассмотрим несколько примеров реализации функций замыкания на нескольких разных языках. Первым в нашем списке стоит Python, что немного странно. Подобно Java, Python использует лямбда-синтаксис для определения анонимных функций, что очень удобно. Просто подумав о том, как часто вы писали анонимную функцию внутри другой функции в качестве Data Scientist, вы можете пролить свет на то, насколько полезны эти функции. Кроме того, весь код, который я собираюсь показать, будет доступен в виде файла внутри моего репозитория случайного кода, вот ссылка:



Во всяком случае, вот наш предыдущий пример, воспроизведенный на Python:

def example2(n):
    h = lambda : print(n)
    return(h)
ourfunc = example2(5)
ourfunc()
5

Этот пример довольно прост для понимания. Мы создаем функцию h с лямбдой, затем возвращаем ее. Python делает это очень просто, в основном это просто понимание лямбды, чтобы понять этот синтаксис. Кстати, если у вас нет четкого понимания лямбда, я написал об этом целую статью! Вот ссылка!:



По сути, все, что лямбда делает для Python, — это позволяет языку создавать анонимные функции. У Джулии тоже есть такая возможность, способов их создания может быть больше, но вот два примера:

f = () -> 5
f()
5
begin
     5
end

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

https://towardsdatascience.com/what-on-earth-is-an-anonymous-function-f8043eb845f3

Когда дело доходит до того же в C, к сожалению, мы быстро понимаем, что выяснить тип возвращаемого значения довольно сложно. Однако это можно сделать немного проще, используя GNU FFCALL. Тем не менее, есть способ сделать это без FFCALL, и это тоже довольно просто, самая большая проблема заключается в том, чтобы получить этот возвращаемый тип. Еще раз, вот наши функции, на этот раз воспроизведенные в C:

#include <stdio.h>
typedef int (*fptr)();
fptr example2 (int n)
{
  void h ()
    { printf("%d", n); }
    return h;
}
int main()
{
    fptr h = example2(5);
    h();
    return 0;
}

Позвольте мне немного объяснить код здесь. Мы начинаем с создания указателя на функцию *fptr, а затем используем его как тип возвращаемого значения, поэтому мы возвращаем указатель на функцию h. Это прекрасно работает и эффективно создает замыкание в C. Как ни странно, многие люди использовали компилятор Apple C для его «блочного» синтаксиса и использовали расширения FFCALL и даже GCC, чтобы добиться того, что можно сделать с этим определением указателя. . Я не уверен, что есть необходимость идти дальше, так как многие примеры, которые будут способствовать этому на других языках, будут очень похожи на первые два, просто либо с дополнительным синтаксисом, либо с другой структурой вызова ( за исключением функциональных языков, таких как SML, например, но… Нет.)

Реальный пример использования

Теперь, когда мы закончили рассматривать замыкания как концепцию, позвольте мне показать вам пример, когда я использовал замыкания в Джулии, чтобы сделать что-то действительно потрясающее. Код, который я собираюсь продемонстрировать, является частью моего проекта Toolips.jl. Сейчас проект находится на ранней стадии, потому что в данный момент у меня очень много дел, а количество других пакетов с открытым исходным кодом, над которыми я также работаю в свободное время, невероятно. Project задуман как модульная структура веб-разработки, которая работает как внутренний и внешний инструмент, но выполняет весь внешний интерфейс с помощью функционального метапрограммирования JavaScript. В любом случае, если вам интересно посмотреть проект или, возможно, поставить ему звезду (я ценю это!), вот ссылка на репозиторий Github:



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

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

С первым замечанием, которое я сделал здесь, должно быть совершенно очевидно, что функции часто используются в качестве данных. Существует техника, аналогичная той, которую я использую внутри OddFrames.jl, используемой здесь, чтобы иметь некоторые частные методы, которые также являются функциями закрытия, поэтому я показываю их:

mutable struct ToolipServer
    ip::String
    port::Integer
    routes::AbstractVector
    remove::Function
    add::Function
    start::Function
    function ToolipServer(ip::String, port::Int64)
        routes = []
        add, remove, start = funcdefs(routes, ip, port)
        new(ip, port, routes, remove, add, start)
    end
function ToolipServer()
        port = 8001
        ip = "127.0.0.1"
        ToolipServer(ip, port)
    end
end

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

function funcdefs(routes::AbstractVector, ip::String, port::Integer)
    add(r::Route) = push!(routes, r)
    remove(i::Int64) = deleteat!(routes, i)
    start() = _start(routes, ip, port)
    return(add, remove, start)
end

Поскольку это функции замыкания, в их соответствующих определениях мы можем использовать все значения, которые определены в исходной функции, которая ее вызывает. Вот почему IP, порт и маршруты предоставляются в качестве аргументов. Часть этого кода, которая ссылается на нашу следующую функцию, снова выделена жирным шрифтом, и вот эта функция:

function _start(routes::AbstractVector, ip::String, port::Integer)
    server = Sockets.listen(Sockets.InetAddr(parse(IPAddr, ip), port))
    println("Starting server on port ", string(port))
    routefunc = generate_router(routes, server)
    @async HTTP.listen(routefunc, ip, port; server = server)
    println("Successfully started Toolips server on port ", port, "\n")
    println("You may visit it now at http://" * string(ip) * ":" * string(port))
    return(server)
end

На данный момент сервер довольно простой, но вы уже можете видеть, как развивается структура. Здесь наиболее важны как вызов generate_router(), так и вызов HTTP.listen(). В HTTP.listen(), который используется для помещения этого сокета в HTTP-соединение, мы предоставляем routefunc в качестве аргумента, который является возвратом generate_router. Одно только это имя, routefunc, вероятно, может помочь предположить, что он на самом деле делает. Однако routefunc — это возврат функции, а не функции. Это связано с тем, что функция не статична, она должна меняться в зависимости от предоставленных ей данных. Вот посмотрите, как именно это выглядит:

function generate_router(routes::AbstractVector, server)
    route_paths = Dict([route.path => route.page for route in routes])
    routeserver = function serve(http)
     HTTP.setheader(http, "Content-Type" => "text/html")
     full_path = split(http.message.target, '?')
     args = ""
     if length(full_path) > 1
         args = full_path[2]
     end
     if fullpath[1] ! in keys(route_paths)
         write(http, generate(route_paths["404"]), args)
     else
         write(http, generate(route_paths[fullpath[1]]), args)
     end
 end # serve()
    return(routeserver)
end

Я выделил как возврат, так и определение routeserver жирным шрифтом, чтобы их было легко увидеть. Функция запускается каждый раз, когда делается запрос, а поток ввода-вывода HTTP предоставляется HTTP в качестве аргумента этой функции. После этого все, что делает эта функция, — генерирует страницу, соответствующую запрошенному кем-то uri. На первый взгляд это действительно просто, но снаружи это становится действительно мощным. Я думаю, что это отличное использование функции замыкания, и, кроме того, я думаю, что это был действительно классный подход к проблеме управления маршрутами и вещами. Я знаю, что некоторым может быть любопытно, почему я решил использовать эту технику, а не использовать HTTP.Router, или, более того, почему я вообще хотел разработать веб-сервер.

Чтобы ответить на первый вопрос, гораздо сложнее обрабатывать множество отдельных маршрутов с помощью HTTP.Router. Кроме того, обработка потоков не может быть настолько чистой, как в этом контексте, а в некоторых случаях пользовательские потоки вообще невозможны. Чтобы ответить на второй вопрос, мы, честно говоря, очень ограничены, когда дело доходит до фреймворков веб-разработки в Джулии. У нас есть вариант Mux.jl и Genie.jl, когда дело доходит до известных пакетов для этой задачи в Джулии. Вот посмотрите на документацию Mux:



Да, верно, так оно и есть.

Я думал, что какое-то время Mux.jl тоже считался устаревшим — или, может быть, это был какой-то другой пакет. В любом случае, несмотря на то, что у нас есть хорошая привязка WebIO/Interact.jl, когда дело доходит до веб-интерактивности, мимы WebIO несколько медленны для оценки, что вполне понятно, но раздражает. Они также гораздо больше ориентированы на научную интерактивность, которой, честно говоря, я думаю, что Interact.jl принадлежит. Interact действительно хорош в этом. Это приложение пакетов. Хотя были веб-приложения, например. Pluto.jl, которые были построены на этом, я думаю, что цель Interact никогда не заключалась в том, чтобы стать серверной частью для полнофункционального веб-приложения (или, может быть, это было, я не уверен?)

На самом деле я был одним из первых пользователей Genie.jl. Genie, вероятно, лучший вариант для веб-разработки Джулии прямо сейчас, есть только некоторые вещи, которые я хотел бы изменить. Самая большая из этих вещей заключается в том, что всю файловую систему очень трудно сделать легковесной. Это во многом напоминает мне аргумент Django против Flask, гораздо проще сказать, что сделать API с Flask, чем с Django, но гораздо проще создать веб-приложение с Django, чем с Flask. Mux.jl уделяет большое внимание тому, чтобы быть реактивным интерфейсным инструментом, и я думаю, что он неплохо справляется с этим приложением. При этом у нас нет действительно микро-фреймворков. При этом я стремлюсь создать это — наряду с уровнем модульности, который позволит дополнить его дополнительными функциями.

Заключение

Возможно, мы немного отклонились от темы при обсуждении Toolips.jl, но суть остается в силе: замыкающие функции невероятно полезны. Я думаю, что отчасти причина, по которой они были так полезны для меня, заключается в том, что у Джулии такие замечательные синтаксические выражения и синтаксис функций. Это значительно упрощает получение этих определений и применение их с пользой. Кроме того, тот факт, что вы можете просто использовать функции в качестве аргументов везде, оценивать и выражать строки, код Julia и многое другое, все это в целом дает довольно надежный интерфейс для написания функций такого рода. Мне также нравится реализация Python, я уверен, что все мы, специалисты по данным, использовали лямбда-выражения раньше, это очень удобно для обработки данных, и если вы когда-либо использовали его внутри другой функции, то вы на самом деле писали замыкающую функцию.

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