PHP para Iniciantes: Funções - Closures, Funções variáveis e Arrow Functions
Não, eu não estou mencionando o dialeto Clojure e sim funções Closure, que você pode encontrar por aí com outros nomes como funções variáveis, arrow functions ou funções anônimas. Na verdade, o termo anônimo é o mais correto, pois ele remete ao sentido de que a função não tem um nome declarado. Isso não é uma exclusividade de funções variáveis, que são variáveis do tipo callable
(eu chamei de variáveis acionáveis num artigo aí). Você verá a seguir três artigos em um: funções variáveis, funções anônimas e arrow functions (PHP 8).
Se você seguir a documentação oficial do PHP à risca, verá que ele apresenta esses tópicos de forma separada. Eu estou indo bem além disso porque acho que é o que faz mais sentido. E comecei te apresentando o termo em inglês pois, se você comete um erro numa função anônima, o que vai aparecer escrito no erro é CLOSURE. Faz o teste e me diz o que aparece na linha 0 do Stack Trace:
<?php
$callable = function() {
throw new Exception();
};
$callable();
Uma das partes mais importantes na vida de um desenvolvedor é o conhecimento da linguagem que está trabalhando, com certeza. Mas o motivo disso não é porque você será contratado para desenvolver coisas supremas, de baixíssimo nível, e sim porque você precisará resolver problemas. Nem sempre a gente fica no mundo perfeito onde as únicas correções feitas são ajustes de regra de negócio, sabe? Na verdade é o contrário, é extremamente raro existir uma posição dessas.
Muitas vezes você toma um erro por algo que não previu. Isso é absolutamente normal. Todos passam por isso, sabia? A questão é que você precisa entender o suficiente para sacar rapidamente o que o interpretador está mostrando. Se eu te ensino somente os termos em português, você iria ler "Closure" e falar "Ahn?"
Mas tudo bem se você não decorar, se você copiar o texto e jogar no Google provavelmente vai aparecer alguém no Stackoverflow com a resposta pronta uma boa pista pra você. Exceto se você realmente trabalha com PHP à nível hardcore, aí tu tá por conta, irmã(o).
Tá bom, Kiko, já entendi. Você vai falar o que é isso?
Sim, eu vou. Perdão pela enrolação, rs.
Um pouco de reflexão
Vamos refletir sobre a seguinte questão: o que difere uma função nomeada de uma função anônima, além do fato de que uma tem nome e outra não?
. . .
Se você pensou em constância, está corretíssimo(a)! E se também pensou em contexto, está mais ainda! No exemplo com Exception
, eu te mostrei como criar uma função variável: usamos a mesmíssima sintaxe de declaração de funções nomeadas, encerrado com um ponto-e-vírgula (;
), certo?
Você pode colocar essa função anônima quase em qualquer lugar:
- numa variável (como no exemplo);
- num índice de um
array
; - no
return
de outra função; - até mesmo em lugar algum, apenas para acioná-la imediatamente.
O último cenário que mencionei se chama IIFE, que significa Immediately Invoked Function Expression, e alguns anos atrás era muito utilizada em linguagens como Javascript. Em PHP, seria mais ou menos assim:
<?php
(function() {
throw new Exception();
})();
O código acima faz a mesma coisa que o primeiro código, exceto que é uma IIFE.
E qual a diferença nos dois casos, Kiko?
Bom, você lembra que alguns artigos atrás eu comentei que o interpretador lê as declarações de funções antes do momento de evaluação? Com um IIFE, você faz com que uma função só seja "detectada" quando aquele trecho do código for interpretado. Você provavelmente nunca irá precisar de algo assim. Afinal, se você atribuir a função a uma variável, o comportamento já será esse.
E se fosse em um
array
, Kiko?
Também seria após o momento de evaluação. Somente as funções nomeadas é que são registradas, por isso que as anônimas só existem na hora de interpretar o código mesmo. E a escrita em array
seria assim:
<?php
$meuArray = [
'minhaFuncaoDeSoma' => function(int $a, int $b): int
{
return $a + $b;
},
];
var_dump($meuArray['minhaFuncaoDeSoma'](5, 10)); // int(15)
E no retorno de uma função, Kiko?
Do mesmo jeito...
<?php // PHP 7+
function obterFuncaoDeImpressao(bool $retornar = false): callable
{
// isso aqui é uma pequena gambiarra... jajá eu mostro o porquê
if($retornar) {
return function (string $texto): string
{
return "Atenção! $texto";
};
}
return function (string $texto): void
{
echo "Atenção! $texto" . PHP_EOL;
};
}
$funcao = obterFuncaoDeImpressao();
$resultado = $funcao("Vai haver revolução!"); // imprime "Atenção! Vai haver revolução!"
var_dump($resultado); // NULL
$funcao = obterFuncaoDeImpressao(true);
$resultado = $funcao("Sentido!"); // não imprime nada
var_dump($resultado); // string(Atenção! Sentido!)
Como a declaração é similar a de uma função nomeada, os escopos também são. Porém, as closures tem uma coisa que as torna super populares: a capacidade de incorporar dados do escopo onde estão sendo geradas.
Por exemplo, na função de cima, eu tenho a possibilidade de gerar uma closure com return
ou gerar outra com echo
, baseada no valor da variável interna $retornar
. Eu poderia ter retornado uma função só e transmitido essa variável para dentro dela, para que ela soubesse o que fazer.
Quando temos essa necessidade, podemos usar o operador use
, que indica que aquela função irá utilizar variáveis externas dentro dela, ficando mais ou menos assim:
<?php // PHP 7+
function obterFuncaoDeImpressao(bool $retornar = false): callable
{
return function (string $texto) use ($retornar): ?string
{
$texto = "Atenção! $texto";
if ($retornar) {
return $texto;
}
echo $texto . PHP_EOL;
return null; // como declarei o tipo de retorno string, precisamos retornar outra coisa aqui
};
}
$funcao = obterFuncaoDeImpressao();
$resultado = $funcao("Vai haver revolução!"); // imprime "Atenção! Vai haver revolução!"
var_dump($resultado); // NULL
$funcao = obterFuncaoDeImpressao(true);
$resultado = $funcao("Sentido!"); // não imprime nada
var_dump($resultado); // string(Atenção! Sentido!)
Legal, não é? Só isso já é um baita benefício que funções nomeadas não tem.
<?php
$conta = "kg_thebest";
// vai dar erro :´(
function mandarLink() use ($conta): void
{
echo "https://twitter.com/$conta";
}
mandarLink();
Legal, Kiko, mas pra que serve essas funções anônimas?
Há alguns padrões de construção (ou Design Patterns) que servem para abstrair e simplificar nosso código. Citando um exemplo clássico, toda empresa tem fluxo de caixa, certo? Para que você possa gerenciar seus custos fixos, variáveis, prejuízos e lucros, é preciso que o sistema de vendas registre cada tipo de venda efetuada. Seja recebendo em cash (leia-se DIÑERO), cartão de débito, cartão de crédito, PIX, o que for. Cada uma dessas formas de pagamento geram taxas diversificadas para o estabelecimento e você pode muito bem fazer um código específico pra cada e direcionar, na mão, pra onde vai cada venda.
Isso funciona, acredite, mas poderia ser melhor. Isso porque se você tratar cada forma de pagamento como forma de venda, você provavelmente fará muita duplicação de código. É preciso abstrair isso: a forma de pagamento é apenas uma informação da venda, não a venda. Enxergando dessa forma, você verá que o que precisa é organizar o código de tal forma que a forma de pagamento te dê a função que você precisa executar para fazer ou registrar a cobrança.
Esse nível de abstração, do tipo me dê o procedimento a executar, se chama Fábrica (leia-se Factory, inclusive indico ler esse site, é SENSACIONAL) e é comumente utilizado para retornar instâncias de forma dinâmica, mas também poderia ser uma fábrica de funções, nada impede, principalmente se você desenvolve em Programação Funcional. Então se você irá gerar N possibilidades de funções, você provavelmente estará retornando closures.
Mesmo citando isso dessa forma, fábricas de closures bem feitas não costumam retornar funções anônimas, e sim referências para funções nomeadas, o que fica bem parecido com o retorno de instâncias de classes específicas.
Uau, Kiko... Quanta coisa!
E eu ainda não acabei! Se você gostou de conhecer as funções anônimas e funções variáveis, deixa só eu te mostrar mais uma coisa iradíssima que o PHP 8 trouxe pra gente...
Arrow Functions
"Funções de seta" (que nome feio MEU DEUSSSS) são funções anônimas reduzidas. Elas não possuem separação de contexto, por isso, a expressão de uma arrow function representa apenas uma instrução de return
.
O motivo de se chamar assim é a sua forma de escrita: fn (...$argumentos) => $retorno;
. Como você pode ver, tem uma seta em ASCII ali dentro (=>
)... E é só isso.
Do mesmo jeito das outras funções, essas possuem os parênteses para incluirmos os argumentos - onde também podemos incluir as tipagens e todas as outras coisas que usamos lá - e também nos possibilita inserir o tipo de retorno, ficando assim: fn (string ...$argumentos): Tipo => $retorno;
E o que isso tem de demais, Kiko?
Bem, arrow functions não possuem separação de contexto, o que significa que ela puxa tudo do escopo de fora para dentro sem a necessidade do operador use
. Com esse carinha, aquele exemplo retornando closure ficaria ainda mais reduzido:
<?php // PHP 8+
function obterFuncaoDeImpressao(bool $retornar = false): callable
{
return fn (string $texto): ?string =>
($texto = "Atenção! $texto") && !$retornar && (print($texto . PHP_EOL))
? null
: $texto;
}
$funcao = obterFuncaoDeImpressao();
$resultado = $funcao("Vai haver revolução!"); // imprime "Atenção! Vai haver revolução!"
var_dump($resultado); // NULL
$funcao = obterFuncaoDeImpressao(true);
$resultado = $funcao("Sentido!"); // não imprime nada
var_dump($resultado); // string(Atenção! Sentido!)
Ok, não foi um bom exemplo de simplicidade, hehe, e eu ainda tive de trocar o echo
por print
, para construir uma equação ternária que possibilitasse as duas execuções em uma única instrução... Ufa!
Mas realmente não ficou um bom exemplo. Para minha sorte, eu resolvi um desafio aqui onde falei um pouco sobre a construção de arrow functions. Lá eu comento até sobre o fato de que você não consegue alterar valores externos à instrução, na prática. Você pode dar uma lida lá se ficou interessade. Se joga no link, ok?!
Voltando às funções de chave...
Funções estáticas
Talvez não faça muito sentido mencionar isso agora, mas uma outra diferença entre as funções nomeadas e anônimas está no seu escopo interno. Quando você cria uma closure dentro de um objeto, ela é vinculada a ele de tal forma que se você acionar a variável pré-definida $this
, você acessará a referência do objeto que construiu a função.
É completamente natural pensar que toda closure deveria ter esse vínculo com o escopo onde está sendo criada, mas não é verdade. O fato é que muitas vezes você não precisa de nenhuma dessas informações. Em casos extremos onde qualquer remoção de referência impacta na performance de execução da aplicação, pode ser uma boa ideia transformar suas closures em funções estáticas!
Ok... O que é isso e pra que serve, Kiko?
Como o próprio nome diz, são funções estáticas, ué. Elas servem para proibir o interpretador de vincular qualquer outra informação à função. Com isso, o interpretador perde menos tempo para fazer o que você está pedindo, rs.
A sintaxe é super complexa: é só adicionar a palavra-chave static
na frente da declaração da função anônima, ficando: static function() { // ...
. Isso também funciona com arrow functions: static fn() => // ...
.
Incluindo referências de outro escopo
Lá no desafio também tem a parte que eu odeio falar, sobre referências em closure. Por favor, me poupem de falar dessa vez, vocês já estão cansades de ler eu reclamando desse tópico, HAHAHAHA. O uso de referências funciona da mesma forma que nos argumentos: operador (&
) antes da variável inserida no operador use
:
<?php
$variavel = "Olá";
$closure = function () use (&$variavel): void
{
$variavel = "Hello";
};
var_dump($variavel); // string(Olá)
// ué?
// pq não mudou, Kiko?
// ...
$closure(); // lembrem sempre de chamar a função, amiguinhos :v
var_dump($variavel); // string(Hello)
Referências em funções de seta
No argumento
<?php
$refNoArgumento = fn (&$arg) => $arg = 10;
$teste = 3;
$refNoArgumento($teste);
var_dump($teste); // int(10)
No retorno
<?php
class Exemplo
{
private int $contador = 0;
public function contadorCabuloso(): callable
{
var_dump(++$this->contador);
return fn&() => $this->contador;
}
}
$exemplo = new Exemplo;
$retornarRef = $exemplo->contadorCabuloso(); // int(1)
$referencia =& $retornarRef(); // recebe o resultado de fn&(), que retorna a referência de lago
$referencia = 10;
$exemplo->contadorCabuloso(); // int(11)
Enfim...
Sobre as funções variáveis, se você tiver dúvida sobre como escrever uma (ou uma forma de referenciar uma função nomeada em uma variável), leia o artigo que mencionei lá em cima, sobre o Tipo de Dados Primitivos - Acionáveis, tem tudo!
E por hoje é só! Curtiu? Comenta e compartilha! Dessa vez eu demorei um pouco mais para concluir esse artigo. Foram quase três horas escrevendo sem parar (milagre, minhas filhas dormiram cedo). Deixa aí aquela força, pois esse foi o último artigo de funções!
No próximo artigo, voltarei com uma introdução para o tópico Classes e Objetos. Estou pensando em explorar um pouco a temática de programação orientada a objetos, mas não sei não, hein? É um mar profundo, rs. Não perca!
Inté!!