Por que um programa usaria um fechamento?

54

Depois de ler muitos posts explicando encerramentos aqui, ainda sinto falta de um conceito-chave: Por que escrever um encerramento? Que tarefa específica um programador estaria realizando que poderia ser melhor atendido por um encerramento?

Exemplos de encerramentos no Swift são acessos de um NSUrl e usando o geocoder reverso. Aqui está um desses exemplos. Infelizmente, esses cursos apenas apresentam o encerramento; eles não explicam por que a solução de código é escrita como um encerramento.

Um exemplo de um problema de programação do mundo real que poderia levar meu cérebro a dizer: "ah, eu deveria escrever um fechamento para isso", seria mais informativo do que uma discussão teórica. Não há escassez de discussões teóricas disponíveis neste site.

    
por Bendrix 05.06.2015 / 15:36
fonte

10 respostas

31

Em primeiro lugar, não há nada que seja impossível sem usar encerramentos. Você sempre pode substituir um fechamento por um objeto que implementa uma interface específica. É apenas uma questão de brevidade e acoplamento reduzido.

Em segundo lugar, tenha em mente que os fechamentos são frequentemente usados de forma inadequada, onde uma simples referência de função ou outro construto seria mais claro. Você não deve tomar todos os exemplos que você vê como uma boa prática.

Onde os closures realmente brilham sobre outros constructos é quando usamos funções de ordem mais alta, quando você realmente precisa para se comunicar, e você pode torná-lo um one-liner, como neste exemplo JavaScript do página da wikipedia sobre fechamentos :

// Return a list of all books with at least 'threshold' copies sold.
function bestSellingBooks(threshold) {
  return bookList.filter(
      function (book) { return book.sales >= threshold; }
    );
}

Aqui, threshold é muito sucinto e naturalmente comunicado de onde é definido para onde é usado. Seu escopo é precisamente limitado o menor possível. filter não precisa ser escrito para permitir a possibilidade de passar dados definidos pelo cliente como um limite. Não precisamos definir estruturas intermediárias com o único objetivo de comunicar o limiar nessa pequena função. É totalmente auto-suficiente.

Você pode escrever isso sem o fechamento, mas isso exigirá muito mais código e será mais difícil de ser seguido. Além disso, o JavaScript tem uma sintaxe lambda razoavelmente detalhada. Em Scala, por exemplo, todo o corpo da função seria:

bookList filter (_.sales >= threshold)

Se você pode, no entanto, usar o ECMAScript 6 , graças ao fat arrow functions até o código JavaScript se torna muito mais simples e pode ser colocado em uma única linha.

const bestSellingBooks = (threshold) => bookList.filter(book => book.sales >= threshold);

No seu próprio código, procure lugares onde você gera muito clichê apenas para comunicar valores temporários de um lugar para outro. Essas são excelentes oportunidades para considerar a substituição por um fechamento.

    
por 16.06.2015 / 19:41
fonte
49

A título de explicação, vou pegar emprestado algum código de este excelente post sobre fechamentos . É JavaScript, mas essa é a linguagem que a maioria das postagens de blogs falam sobre o uso de closures, porque os closures são tão importantes em JavaScript.

Digamos que você queira renderizar uma matriz como uma tabela HTML. Você poderia fazer assim:

function renderArrayAsHtmlTable (array) {
  var table = "<table>";
  for (var idx in array) {
    var object = array[idx];
    table += "<tr><td>" + object + "</td></tr>";
  }
  table += "</table>";
  return table;
}

Mas você está à mercê do JavaScript para saber como cada elemento da matriz será processado. Se você quisesse controlar a renderização, poderia fazer isso:

function renderArrayAsHtmlTable (array, renderer) {
  var table = "<table>";
  for (var idx in array) {
    var object = array[idx];
    table += "<tr><td>" + renderer(object) + "</td></tr>";
  }
  table += "</table>";
  return table;
}

E agora você pode simplesmente passar uma função que retorna a renderização desejada.

E se você quisesse exibir um total em execução em cada linha da tabela? Você precisaria de uma variável para rastrear esse total, não é? Um encerramento permite que você escreva uma função de renderizador que fecha a variável total em execução e permite que você escreva um renderizador que possa acompanhar o total em execução:

function intTableWithTotals (intArray) {
  var total = 0;
  var renderInt = function (i) {
    total += i;
    return "Int: " + i + ", running total: " + total;
  };
  return renderObjectsInTable(intArray, renderInt);
}

A mágica que está acontecendo aqui é que renderInt retém o acesso à variável total , mesmo que renderInt seja repetidamente chamado e saia.

Em uma linguagem mais tradicionalmente orientada a objetos do que JavaScript, você poderia escrever uma classe que contenha essa variável total e passar isso ao invés de criar um fechamento. Mas um fechamento é uma maneira muito mais poderosa, limpa e elegante de fazê-lo.

Leitura Adicional

por 05.06.2015 / 16:27
fonte
22

O propósito de closures é simplesmente preservar estado; daí o nome closure - ele fecha sobre estado. Para a facilidade de mais explicações, usarei o Javascript.

Normalmente você tem uma função

function sayHello(){
    var txt="Hello";
    return txt;
}

onde o escopo da (s) variável (s) está ligado a essa função. Então, após a execução, a variável txt sai do escopo. Não há como acessá-lo ou usá-lo após a execução da função.

Fechamentos são construções de linguagem, que permitem - como dito anteriormente - preservar o estado das variáveis e, assim, prolongar o escopo.

Isso pode ser útil em diferentes casos. Um caso de uso é a construção de funções de maior ordem .

In mathematics and computer science, a higher-order function (also functional form, functional or functor) is a function that does at least one of the following:1

  • takes one or more functions as an input
  • outputs a function

Um exemplo simples, mas não muito útil, é:

 makeadder=function(a){
     return function(b){
         return a+b;
     }
 }

 add5=makeadder(5);
 console.log(add5(10)); 

Você define uma função makedadder , que recebe um parâmetro como entrada e retorna uma função . Existe uma função outer function(a){} e interior function(b){}{} . Além disso, você define (implicitamente) outra função add5 como resultado de chamar a função de ordem mais alta% código%. makeadder retorna uma função anônima ( inner ), que por sua vez recebe 1 parâmetro e retorna a soma do parâmetro da função outer e o parâmetro função interna .

O truque é que, enquanto retorna a função inner , que faz a adição real, o escopo do parâmetro da função externa ( makeadder(5) ) é preservado . a lembra que o parâmetro add5 foi a .

Ou para mostrar um exemplo pelo menos de alguma forma útil:

  makeTag=function(openTag, closeTag){
     return function(content){
         return openTag +content +closeTag;
     }
 }

 table=makeTag("<table>","</table>")
 tr=makeTag("<tr>", "</tr>");
 td=makeTag("<td>","</td>");
 console.log(table(tr(td("I am a Row"))));

Outro exemplo comum é a assim chamada IIFE = chamada imediatamente da expressão de função. É muito comum em javascript as variáveis de membros privados fake . Isto é feito através de uma função, que cria um private scope = 5 , porque é imediatamente após a definição chamada. A estrutura é closure . Observe os colchetes function(){}() após a definição. Isso possibilita usá-lo para criação de objetos com o padrão de módulo de revelação . O truque é criar um escopo e retornar um objeto que tenha acesso a esse escopo após a execução do IIFE.

O exemplo de Addi é assim:

 var myRevealingModule = (function () {

         var privateVar = "Ben Cherry",
             publicVar = "Hey there!";

         function privateFunction() {
             console.log( "Name:" + privateVar );
         }

         function publicSetName( strName ) {
             privateVar = strName;
         }

         function publicGetName() {
             privateFunction();
         }


         // Reveal public pointers to
         // private functions and properties

         return {
             setName: publicSetName,
             greeting: publicVar,
             getName: publicGetName
         };

     })();

 myRevealingModule.setName( "Paul Kinlan" );

O objeto retornado tem referências a funções (por exemplo, () ), que por sua vez têm acesso a variáveis "privadas" publicSetName .

Mas estes são casos de uso mais especiais para Javascript.

What specific task would a programmer be performing that might be best served by a closure?

Existem várias razões para isso. Pode ser que seja natural para ele, já que ele segue um paradigma funcional . Ou em Javascript: é mera necessidade confiar em encerramentos para contornar algumas peculiaridades da linguagem.

    
por 05.06.2015 / 19:20
fonte
16

Existem dois casos de uso principais para encerramentos:

  1. Asynchrony. Digamos que você queira executar uma tarefa que demore um pouco e, em seguida, faça alguma coisa quando estiver concluída. Você pode fazer seu código esperar que isso seja feito, o que bloqueia a execução adicional e pode deixar seu programa sem resposta ou chamar sua tarefa de forma assíncrona e dizer "inicie esta tarefa longa em segundo plano e, quando ela for concluída, execute esse fechamento", onde o fechamento contém o código a ser executado quando é feito.

  2. Callbacks. Eles também são conhecidos como "delegados" ou "manipuladores de eventos", dependendo do idioma e da plataforma. A idéia é que você tenha um objeto personalizável que, em certos pontos bem definidos, irá executar um evento , que executa um fechamento passado pelo código que o configura. Por exemplo, na interface do usuário do seu programa, você pode ter um botão e dar a ele um fechamento que mantenha o código a ser executado quando o usuário clicar no botão.

Existem vários outros usos para encerramentos, mas esses são os dois principais.

    
por 05.06.2015 / 16:09
fonte
13

Alguns outros exemplos:

Classificando
A maioria das funções de classificação opera comparando pares de objetos. Alguma técnica de comparação é necessária. Restringir a comparação a um operador específico significa uma classificação bastante inflexível. Uma abordagem muito melhor é receber uma função de comparação como um argumento para a função de classificação. Às vezes, uma função de comparação sem estado funciona bem (por exemplo, classificando uma lista de números ou nomes), mas e se a comparação precisar de estado?

Por exemplo, considere classificar uma lista de cidades por distância para algum local específico. Uma solução feia é armazenar as coordenadas desse local em uma variável global. Isso faz a própria função de comparação sem estado, mas ao custo de uma variável global.

Essa abordagem impede que vários encadeamentos classifiquem simultaneamente a mesma lista de cidades por sua distância para dois locais diferentes. Um fechamento que envolve o local resolve esse problema e elimina a necessidade de uma variável global.


Números aleatórios
O original rand() não recebeu argumentos. Geradores de números pseudo-aleatórios precisam de estado. Alguns (por exemplo, Mersenne Twister) precisam de muito estado. Mesmo o simples, mas terrível rand() precisava de estado. Leia um artigo de matemática em um novo gerador de números aleatórios e você inevitavelmente verá variáveis globais. Isso é bom para os desenvolvedores da técnica, não tão bom para os chamadores. Encapsular esse estado em uma estrutura e passar a estrutura para o gerador de números aleatórios é uma maneira de contornar o problema de dados globais. Esta é a abordagem usada em muitas linguagens não-OO para tornar um gerador de números aleatórios reentrante. Um encerramento oculta esse estado do chamador. Um fechamento oferece a sequência de chamada simples de rand() e a reentrada do estado encapsulado.

Há mais números aleatórios do que apenas um PRNG. A maioria das pessoas que querem aleatoriedade quer distribuir de uma determinada maneira. Vou começar com números sorteados entre 0 e 1, ou U (0,1) para breve. Qualquer PRNG que gere números inteiros entre 0 e algum máximo será suficiente; simplesmente divida (como um ponto flutuante) o inteiro aleatório pelo máximo. Uma maneira conveniente e genérica de implementar isso é criar um fechamento que receba um fechamento (o PRNG) e o máximo como entradas. Agora temos um gerador aleatório genérico e fácil de usar para U (0,1).

Existem várias outras distribuições além de U (0,1). Por exemplo, uma distribuição normal com uma certa média e desvio padrão. Todo algoritmo do gerador de distribuição normal que eu utilizo usa um gerador U (0,1). Uma maneira conveniente e genérica de criar um gerador normal é criar um fechamento que encapsule o gerador U (0,1), a média e o desvio padrão como estado. Isto é, pelo menos conceitualmente, um fechamento que leva um fechamento que leva um encerramento como um argumento.

    
por 05.06.2015 / 22:37
fonte
7

Closures são equivalentes a objetos que implementam um método run () e, inversamente, objetos podem ser emulados com closures.

  • A vantagem dos encerramentos é que eles podem ser usados facilmente em qualquer lugar que você espere uma função: funções de ordem superior, retornos de chamada simples (ou Padrão de Estratégia). Você não precisa definir uma interface / classe para criar fechamentos ad-hoc.

  • A vantagem dos objetos é a possibilidade de interações mais complexas: vários métodos e / ou interfaces diferentes.

Então, usar closure ou objetos é principalmente uma questão de estilo. Aqui está um exemplo de coisas que os fechamentos facilitam, mas é inconveniente para implementar com objetos:

 (let ((seen))
    (defun register-name (name)
       (pushnew name seen :test #'string=))

    (defun all-names ()
       (copy-seq seen))

    (defun reset-name-registry ()
       (setf seen nil)))

Basicamente, você encapsula um estado oculto que é acessado apenas através de encerramentos globais: você não precisa se referir a nenhum objeto, apenas use o protocolo definido pelas três funções.

Eu confio no primeiro comentário do supercat sobre o fato de que, em alguns idiomas, é possível controlar precisamente o tempo de vida dos objetos, enquanto o mesmo não é verdadeiro para closures. No caso de linguagens coletoras de lixo, no entanto, a vida útil dos objetos geralmente é ilimitada e, portanto, é possível construir um fechamento que poderia ser chamado em um contexto dinâmico onde não deveria ser chamado (leitura de um fechamento após um fluxo está fechado, por exemplo).

No entanto, é bastante simples evitar esse uso incorreto, capturando uma variável de controle que protegerá a execução de um encerramento. Mais precisamente, aqui está o que tenho em mente (em Common Lisp):

(defun guarded (function)
  (let ((active t))
    (values (lambda (&rest args)
              (when active
                (apply function args)))
            (lambda ()
              (setf active nil)))))

Aqui, pegamos um designador de função function e retornamos dois closures, ambos capturando uma variável local chamada active :

  • o primeiro delegado para function , somente quando active for verdadeiro
  • o segundo define action a nil , a.k.a. false .

Em vez de (when active ...) , é claro que é possível ter uma expressão (assert active) , que poderia lançar uma exceção caso o fechamento seja chamado quando não deveria. Além disso, lembre-se de que o código inseguro já pode lançar uma exceção sozinho quando usado incorretamente, de modo que você raramente precisa desse tipo de wrapper.

Veja como você pode usá-lo:

(use-package :metabang-bind) ;; for bind

(defun example (obj1 obj2)
  (bind (((:values f f-deactivator)(guarded (lambda () (do-stuff obj1))))
         ((:values g g-deactivator)(guarded (lambda () (do-thing obj2)))))

    ;; ensure the closure are inactive when we exit
    (unwind-protect
         ;; pass closures to other functions
         (progn
           (do-work f)
           (do-work g))

      ;; cleanup code: deactivate closures
      (funcall f-deactivator)
      (funcall g-deactivator))))

Note que os encerramentos desativados também podem ser dados a outras funções; aqui, as variáveis active locais não são compartilhadas entre f e g ; Além disso, além de active , f refere-se apenas a obj1 e g refere-se apenas a obj2 .

O outro ponto mencionado pelo supercat é que os closures podem levar a vazamentos de memória, mas, infelizmente, é o caso de quase tudo em ambientes de coleta de lixo. Se estiverem disponíveis, isso pode ser resolvido por ponteiros fracos (o fechamento em si pode ser mantido na memória, mas não impede a coleta de lixo de outros recursos).

    
por 05.06.2015 / 19:30
fonte
6

Nada que não tenha sido dito, mas talvez um exemplo mais simples.

Veja um exemplo de JavaScript usando tempos limite:

// Example function that logs something to the browser's console after a given delay
function delayedLog(message, delay) {
  // this function will be called when the timer runs out
  var fire = function () {
    console.log(message); // closure magic!
  };

  // set a timeout that'll call fire() after a delay
  setTimeout(fire, delay);
}

O que acontece aqui é que, quando delayedLog() é chamado, ele retorna imediatamente depois de definir o tempo limite, e o tempo limite continua diminuindo em segundo plano.

Mas quando o tempo limite acabar e chamar a função fire() , o console exibirá o message que foi originalmente passado para delayedLog() , porque ele ainda estará disponível para fire() por meio do encerramento. Você pode chamar delayedLog() o quanto quiser, com uma mensagem diferente e atrasar cada vez, e isso fará a coisa certa.

Mas vamos imaginar que o JavaScript não tenha encerramentos.

Uma maneira seria fazer setTimeout() bloquear - mais como uma função "sleep" - então o escopo de delayedLog() não desaparece até que o tempo limite tenha acabado. Mas bloquear tudo não é muito legal.

Outra maneira seria colocar a variável message em algum outro escopo que será acessível após o escopo do delayedLog() desaparecer.

Você pode usar variáveis globais - ou pelo menos "com escopo mais amplo", mas precisa descobrir como acompanhar a mensagem com o tempo limite. Mas não pode ser apenas uma fila FIFO sequencial, porque você pode definir qualquer atraso desejado. Então pode ser "primeiro, terceiro fora" ou algo assim. Então, você precisaria de outros meios para vincular uma função cronometrada às variáveis necessárias.

Você pode instanciar um objeto de tempo limite que "agrupa" o cronômetro com a mensagem. O contexto de um objeto é mais ou menos um escopo que fica ao redor. Então você teria o temporizador executado no contexto do objeto, então ele teria acesso à mensagem certa. Mas você teria que armazenar esse objeto porque, sem nenhuma referência, seria coletado lixo (sem encerramentos, também não haveria referências implícitas a ele). E você teria que remover o objeto uma vez que ele fosse desativado, caso contrário, ele ficará por perto. Então, você precisaria de algum tipo de lista de objetos de tempo limite e periodicamente verificá-los quanto a objetos "gastos" a serem removidos - ou os objetos seriam adicionados e removidos da lista, e ...

Então ... sim, isso está ficando chato.

Felizmente, você não precisa usar um escopo mais amplo ou disputar objetos apenas para manter determinadas variáveis. Como o JavaScript tem encerramentos, você já tem exatamente o escopo de sua necessidade. Um escopo que fornece acesso à variável message quando você precisar. E por causa disso, você pode escrever delayedLog() como acima.

    
por 06.06.2015 / 13:25
fonte
3
O

PHP pode ser usado para ajudar a mostrar um exemplo real em um idioma diferente.

protected function registerRoutes($dic)
{
  $router = $dic['router'];

  $router->map(['GET','OPTIONS'],'/api/users',function($request,$response) use ($dic)
  {
    $controller = $dic['user_api_controller'];
    return $controller->findAllAction($request,$response);
  })->setName('api_users');
}

Então, basicamente, estou registrando uma função que será executada para o / api / users URI . Esta é na verdade uma função de middleware que acaba sendo armazenada em uma pilha. Outras funções serão envolvidas em torno dele. Praticamente como Node.js / Express.js faz.

O contêiner injeção de dependência está disponível (por meio da cláusula use) dentro da função quando é chamado. É possível fazer algum tipo de classe de ação de rota, mas esse código é mais simples, rápido e fácil de manter.

    
por 05.06.2015 / 23:37
fonte
-1

Um fechamento é um pedaço de código arbitrário, incluindo variáveis, que podem ser manipuladas como dados de primeira classe.

Um exemplo trivial é o qsort antigo: é uma função para classificar dados. Você tem que dar um ponteiro para uma função que compara dois objetos. Então você tem que escrever uma função. Essa função pode precisar ser parametrizada, o que significa que você fornece variáveis estáticas. O que significa que não é thread-safe. Você está no DS. Então você escreve uma alternativa que leva um fechamento em vez de um ponteiro de função. Você resolve instantaneamente o problema de parametrização porque os parâmetros se tornam parte do fechamento. Você torna seu código mais legível porque você escreve como os objetos são comparados diretamente com o código que chama a função de classificação.

Existem várias situações em que você deseja executar alguma ação que exija uma boa quantidade de código de placa de caldeira, além de um pequeno, mas essencial, código que precisa ser adaptado. Você evita o código clichê escrevendo uma função uma vez que recebe um parâmetro de fechamento e faz todo o código clichê ao redor dele, e então você pode chamar essa função e passar o código para ser adaptado como um encerramento. Uma maneira muito compacta e legível para escrever código.

Você tem uma função em que algum código não trivial precisa ser executado em muitas situações diferentes. Isso costumava produzir duplicação de código ou código contorcido para que o código não trivial estivesse presente apenas uma vez. Trivial: Você atribui um fechamento a uma variável e a chama da maneira mais óbvia, onde quer que seja necessário.

Multithreading: iOS / MacOS X tem funções para fazer coisas como "executar esse fechamento em um thread de segundo plano", "... no thread principal", "... no thread principal, daqui a 10 segundos". Faz o multithreading trivial .

Chamadas assíncronas: Foi o que o OP viu. Qualquer chamada que acesse a internet, ou qualquer outra coisa que possa levar tempo (como ler coordenadas de GPS) é algo que você não pode esperar pelo resultado. Então você tem funções que fazem coisas em segundo plano, e então você passa um fechamento para dizer a elas o que fazer quando elas terminarem.

Esse é um pequeno começo. Cinco situações em que os fechamentos são revolucionários em termos de produzir código compacto, legível, confiável e eficiente.

    
por 13.05.2016 / 22:38
fonte
-4

Um encerramento é uma forma abreviada de escrever um método onde ele deve ser usado. Isso poupa o esforço de declarar e escrever um método separado. É útil quando o método será usado apenas uma vez e a definição do método for curta. Os benefícios são reduzidos, pois não há necessidade de especificar o nome da função, seu tipo de retorno ou seu modificador de acesso. Além disso, ao ler o código, você não precisa procurar em outro lugar pela definição do método.

O texto acima é um resumo de Understand Lambda Expressions de Dan Avidar.

Isso esclareceu o uso de encerramentos para mim porque esclarece as alternativas (fechamento vs. método) e os benefícios de cada um.

O código a seguir é usado uma vez e somente uma vez durante a configuração. Escrevê-lo no lugar sob viewDidLoad evita o trabalho de procurá-lo em outro lugar e reduz o tamanho do código.

myPhoton!.getVariable("Temp", completion: { (result:AnyObject!, error:NSError!) -> Void in
  if let e = error {
    self.getTempLabel.text = "Failed reading temp"
  } else {
    if let res = result as? Float {
    self.getTempLabel.text = "Temperature is \(res) degrees"
    }
  }
})

Além disso, ele permite que um processo assíncrono seja concluído sem bloquear outras partes do programa, e um fechamento manterá um valor para reutilização em chamadas de função subseqüentes.

Outro encerramento; esse aqui captura um valor ...

let animals = ["fish", "cat", "chicken", "dog"]
let sortedStrings = animals.sorted({ (one: String, two: String) -> Bool in return one > two
}) println(sortedStrings)
    
por 17.06.2015 / 19:22
fonte