Um combinador Y é um conceito de ciência da computação do lado "funcional" das coisas. A maioria dos programadores não sabe muito sobre combinadores, mesmo que tenha ouvido falar deles.
- O que é um combinador em Y?
- Como os combinadores funcionam?
- Para que servem?
- Eles são úteis em linguagens processuais?
functional-programming
computer-science
theory
definition
combinators
Chris Ammerman
fonte
fonte
Respostas:
Se você estiver pronto para uma longa leitura, Mike Vanier tem uma ótima explicação . Para encurtar a história, permite implementar a recursão em um idioma que não necessariamente o suporta nativamente.
fonte
Um combinador Y é um "funcional" (uma função que opera em outras funções) que permite a recursão, quando você não pode se referir à função por dentro. Na teoria da ciência da computação, ela generaliza a recursão , abstraindo sua implementação e, assim, separando-a do trabalho real da função em questão. O benefício de não precisar de um nome em tempo de compilação para a função recursiva é uma espécie de bônus. =)
Isso é aplicável em idiomas que suportam funções lambda . A natureza baseada na expressão das lambdas geralmente significa que elas não podem se referir a si mesmas pelo nome. E contornar isso por meio da declaração da variável, fazendo referência a ela e atribuindo o lambda a ela, para completar o loop de auto-referência, é frágil. A variável lambda pode ser copiada e a variável original reatribuída, o que interrompe a auto-referência.
Os combinadores Y são complicados de implementar, e costumam usar, em linguagens de tipo estático (que costumam ser linguagens procedurais ), porque geralmente as restrições de digitação exigem que o número de argumentos para a função em questão seja conhecido em tempo de compilação. Isso significa que um combinador y deve ser escrito para qualquer contagem de argumentos que você precise usar.
Abaixo está um exemplo de como o uso e o funcionamento de um Y-Combinator, em C #.
O uso de um combinador Y envolve uma maneira "incomum" de construir uma função recursiva. Primeiro, você deve escrever sua função como um pedaço de código que chama uma função preexistente, e não ela mesma:
Em seguida, você transforma isso em uma função que recebe uma função para chamar e retorna uma função que faz isso. Isso é chamado de funcional porque leva uma função e executa uma operação que resulta em outra função.
Agora você tem uma função que assume uma função e retorna outra função que parece um fatorial, mas, em vez de se chamar, chama o argumento passado para a função externa. Como você faz disso o fatorial? Passe a função interna para si mesma. O Y-Combinator faz isso, sendo uma função com um nome permanente, que pode introduzir a recursão.
Em vez da chamada fatorial em si, o que acontece é que o fatorial chama o gerador fatorial (retornado pela chamada recursiva ao Y-Combinator). E, dependendo do valor atual de t, a função retornada pelo gerador chamará o gerador novamente, com t - 1, ou apenas retornará 1, encerrando a recursão.
É complicado e enigmático, mas tudo acontece em tempo de execução, e a chave para o seu trabalho é a "execução adiada" e o rompimento da recursão para abranger duas funções. O F interno é passado como argumento , a ser chamado na próxima iteração, apenas se necessário .
fonte
fix :: (a -> a) -> a
e,a
por sua vez, pode ser uma função de quantos argumentos você desejar. Isso significa que a digitação estática realmente não torna isso complicado.Eu tirei isso de http://www.mail-archive.com/[email protected]/msg02716.html, que é uma explicação que escrevi há vários anos.
Vou usar JavaScript neste exemplo, mas muitos outros idiomas também funcionarão.
Nosso objetivo é ser capaz de escrever uma função recursiva de 1 variável usando apenas funções de 1 variável e sem atribuições, definindo as coisas pelo nome etc. é dado.) Parece impossível, não é? Como exemplo, vamos implementar fatorial.
Bem, o primeiro passo é dizer que poderíamos fazer isso facilmente se trapacearmos um pouco. Usando funções de 2 variáveis e atribuição, podemos pelo menos evitar o uso de atribuição para configurar a recursão.
Agora vamos ver se podemos trapacear menos. Bem, primeiro estamos usando a atribuição, mas não precisamos. Podemos apenas escrever X e Y em linha.
Mas estamos usando funções de 2 variáveis para obter uma função de 1 variável. Podemos consertar isso? Bem, um cara esperto chamado Haskell Curry tem um truque legal, se você tiver boas funções de ordem superior, precisará apenas de uma variável. A prova é que você pode obter das funções de 2 (ou mais no caso geral) variáveis para 1 variável com uma transformação de texto puramente mecânica como esta:
onde ... permanece exatamente o mesmo. (Esse truque é chamado de "currying" em homenagem ao seu inventor. A linguagem Haskell também é nomeada para Haskell Curry. Arquivo que é feito sob trivialidades inúteis.) Agora basta aplicar essa transformação em todos os lugares e obteremos a versão final.
Sinta-se livre para experimentar. alert () que retornar, amarre-o a um botão, o que for. Esse código calcula fatoriais, recursivamente, sem usar atribuições, declarações ou funções de 2 variáveis. (Mas tentar rastrear como ele funciona provavelmente fará sua cabeça girar. E entregá-la, sem a derivação, apenas um pouco reformatada resultará em um código que certamente irá confundir e confundir.)
Você pode substituir as 4 linhas que definem recursivamente o fatorial por qualquer outra função recursiva desejada.
fonte
function (n) { return builder(builder)(n);}
vez debuilder(builder)
?Gostaria de saber se há alguma utilidade em tentar construir isso a partir do zero. Vamos ver. Aqui está uma função fatorial básica e recursiva:
Vamos refatorar e criar uma nova função chamada
fact
que retorna uma função de computação fatorial anônima em vez de executar o próprio cálculo:Isso é um pouco estranho, mas não há nada de errado nisso. Estamos apenas gerando uma nova função fatorial a cada etapa.
A recursão nesse estágio ainda é bastante explícita. A
fact
função precisa estar ciente de seu próprio nome. Vamos parametrizar a chamada recursiva:Isso é ótimo, mas
recurser
ainda precisa saber o próprio nome. Vamos parametrizar isso também:Agora, em vez de chamar
recurser(recurser)
diretamente, vamos criar uma função wrapper que retorne seu resultado:Agora podemos nos livrar
recurser
completamente do nome; é apenas um argumento para a função interna de Y, que pode ser substituída pela própria função:O único nome externo ainda referenciado é
fact
, mas agora deve ficar claro que também é facilmente parametrizado, criando a solução completa e genérica:fonte
recurser
. Não é a menor idéia do que está fazendo ou por quê.recurser
função é o primeiro passo em direção a esse objetivo, porque nos fornece uma versão recursivafact
que nunca se refere ao nome.function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });
. E é assim que eu digeri (não tenho certeza se está correto): não fazendo referência explícita à função (não permitida como um combinador ), podemos usar duas funções parcialmente aplicadas / com curry (uma função criadora e a função calcular), com quais podemos criar funções lambda / anônimas que obtêm recursivas sem a necessidade de um nome para a função de cálculo?A maioria das respostas acima descrever o que o Y-Combinator é , mas não o que é para .
Os combinadores de ponto fixo são usados para mostrar que o cálculo lambda está completo . Este é um resultado muito importante na teoria da computação e fornece uma base teórica para a programação funcional .
Estudar combinadores de ponto fixo também me ajudou a entender realmente a programação funcional. Eu nunca encontrei nenhum uso para eles na programação real.
fonte
combinador y em JavaScript :
Edit : Eu aprendo muito olhando o código, mas este é um pouco difícil de engolir sem algum histórico - desculpe por isso. Com algum conhecimento geral apresentado por outras respostas, você pode começar a separar o que está acontecendo.
A função Y é o "combinador y". Agora dê uma olhada na
var factorial
linha onde Y é usado. Observe que você passa uma função para ela que possui um parâmetro (neste exemplorecurse
) que também é usado posteriormente na função interna. O nome do parâmetro basicamente se torna o nome da função interna, permitindo que ele execute uma chamada recursiva (uma vez que utilizarecurse()
em sua definição.) O combinador y executa a mágica de associar a função interna anônima ao nome do parâmetro da função transmitida para Y.Para obter uma explicação completa de como Y faz a mágica, confira o artigo vinculado (não por mim).
fonte
arguments.callee
não é permitido no modo estrito: developer.mozilla.org/en/JavaScript/…(function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
Para programadores que não encontraram a programação funcional em profundidade e não querem começar agora, mas são levemente curiosos:
O combinador Y é uma fórmula que permite implementar a recursão em uma situação em que as funções não podem ter nomes, mas podem ser passadas como argumentos, usadas como valores de retorno e definidas em outras funções.
Funciona passando a função para si mesma como argumento, para que possa se chamar.
Faz parte do cálculo lambda, que é realmente matemática, mas é efetivamente uma linguagem de programação e é bastante fundamental para a ciência da computação e especialmente para a programação funcional.
O valor prático diário do combinador Y é limitado, pois as linguagens de programação tendem a permitir que você nomeie funções.
Caso você precise identificá-lo em uma fila da polícia, fica assim:
Geralmente, você pode identificá-lo por causa dos repetidos
(λx.f (x x))
.Os
λ
símbolos são a letra grega lambda, que dá nome ao cálculo lambda, e há muitos(λx.t)
termos de estilo porque é assim que o cálculo lambda se parece.fonte
U x = x x
,Y = U . (. U)
(abusando da notação de Haskell). IOW, com combinadores adequadosY = BU(CBU)
,. AssimYf = U (f . U) = (f . U) (f . U) = f (U (f . U)) = f ((f . U) (f . U))
,.Recursão anônima
Um combinador de ponto fixo é uma função de ordem superior
fix
que, por definição, satisfaz a equivalênciafix f
representa uma soluçãox
para a equação de ponto fixoO fatorial de um número natural pode ser comprovado por
Usando
fix
, provas construtivas arbitrárias sobre funções gerais / μ-recursivas podem ser derivadas sem auto-referencialidade anônima.Onde
de tal modo que
Esta prova formal de que
usa metodicamente a equivalência do combinador de ponto fixo para regravações
Cálculo lambda
O formalismo do cálculo lambda não tipado consiste em uma gramática livre de contexto
onde
v
varia sobre variáveis, junto com as regras de redução beta e etaA redução beta substitui todas as ocorrências livres da variável
x
no corpo de abstração ("função")B
pela expressão ("argumento")E
. A redução de Eta elimina a abstração redundante. Às vezes é omitido do formalismo. Uma expressão irredutível , à qual não se aplica regra de redução, está na forma normal ou canônica .é uma abreviação de
(multiariedade de abstração),
é uma abreviação de
(associatividade à esquerda do aplicativo),
e
são equivalentes a alfa .
Abstração e aplicação são as duas únicas "primitivas de linguagem" do cálculo lambda, mas permitem a codificação de dados e operações arbitrariamente complexos.
Os numerais da Igreja são uma codificação dos números naturais semelhantes aos naturais Peano-axiomáticos.
Uma prova formal de que
usando a regra de reescrita da redução beta:
Combinadores
No cálculo lambda, combinadores são abstrações que não contêm variáveis livres. Mais simplesmente
I
:, o combinador de identidadesisomórfico para a função de identidade
Tais combinadores são os operadores primitivos dos cálculos combinadores, como o sistema SKI.
A redução beta não está fortemente normalizando ; nem todas as expressões redutíveis, "redexes", convergem para a forma normal sob redução beta. Um exemplo simples é a aplicação divergente do
ω
combinador ômegapara si mesmo:
A redução das subexpressões mais à esquerda (“cabeças”) é priorizada. A ordem aplicativa normaliza os argumentos antes da substituição, a ordem normal não. As duas estratégias são análogas à avaliação ágil, por exemplo, C, e à preguiçosa, como Haskell.
diverge sob redução beta de ordem do aplicativo
desde em semântica estrita
mas converge com a redução beta de ordem normal preguiçosa
Se uma expressão tiver uma forma normal, a redução beta de ordem normal a encontrará.
Y
A propriedade essencial do
Y
combinador de ponto fixoÉ dado por
A equivalência
é isomórfico para
O cálculo lambda não tipado pode codificar provas construtivas arbitrárias sobre funções gerais / μ-recursivas.
(Multiplicação atrasada, confluência)
Para o cálculo lambda não tipificado da Igreja, foi demonstrado que existe uma infinidade recursivamente enumerável de combinadores de ponto fixo
Y
.A redução beta de ordem normal torna o cálculo lambda não digitado sem extensão um sistema de reescrita completo de Turing.
Em Haskell, o combinador de ponto fixo pode ser implementado com elegância
A preguiça de Haskell normaliza para uma finura antes de todas as subexpressões terem sido avaliadas.
fonte
λ x . x
, como você está hoje?Outras respostas fornecem respostas bastante concisas para isso, sem um fato importante: você não precisa implementar o combinador de ponto fixo em nenhuma linguagem prática dessa maneira complicada e isso não serve para nenhum propósito prático (exceto "veja, eu sei o que o combinador Y é"). É um conceito teórico importante, mas de pouco valor prático.
fonte
Aqui está uma implementação em JavaScript do Y-Combinator e da função fatorial (do artigo de Douglas Crockford, disponível em: http://javascript.crockford.com/little.html ).
fonte
Um combinador em Y é outro nome para um capacitor de fluxo.
fonte
Eu escrevi uma espécie de "guia de idiotas" para o Y-Combinator, tanto em Clojure quanto em Scheme, a fim de me ajudar a lidar com isso. Eles são influenciados pelo material de "The Little Schemer"
No esquema: https://gist.github.com/z5h/238891
ou Clojure: https://gist.github.com/z5h/5102747
Ambos os tutoriais são intercalados com comentários e devem ser recortados e colados no seu editor favorito.
fonte
Como iniciante nos combinadores, achei o artigo de Mike Vanier (obrigado Nicholas Mancuso) muito útil. Eu gostaria de escrever um resumo, além de documentar minha compreensão, se isso puder ser útil para alguns outros, eu ficaria muito feliz.
De porcaria a menos porcaria
Usando o fatorial como exemplo, usamos a seguinte
almost-factorial
função para calcular o fatorial do númerox
:No pseudo-código acima,
almost-factorial
recebe funçãof
e númerox
(almost-factorial
é curry, para que possa ser visto como tendo funçãof
e retornando uma função de 1 aridade).Quando
almost-factorial
calcula fatorial parax
, delega o cálculo de fatorial parax - 1
funcionarf
e acumula esse resultado comx
(nesse caso, multiplica o resultado de (x - 1) com x).Pode ser visto como
almost-factorial
uma versão de baixa qualidade da função fatorial (que só pode calcular o número de caixax - 1
) e retorna uma versão menos ruim de fatorial (que calcula o número de caixax
). Como nesta forma:Se passarmos repetidamente a versão menos cagada do fatorial para
almost-factorial
, obteremos a função fatorial desejadaf
. Onde pode ser considerado como:Ponto de correção
O fato de que isso
almost-factorial f = f
significaf
é o ponto de correção da funçãoalmost-factorial
.Essa foi uma maneira realmente interessante de ver as relações das funções acima e foi um momento de aha para mim. (leia a postagem de Mike no ponto de correção, se você não tiver)
Três funções
Para generalizar, temos uma função não recursiva
fn
(como nosso quase fatorial), temos sua função de ponto de correçãofr
(como nosso f); então, o queY
faz é quando você doaY
fn
,Y
retorna a função de ponto de correção defn
.Então, em resumo (simplificado assumindo que
fr
leva apenas um parâmetro;x
degenera parax - 1
,x - 2
... em recursão):fn
:def fn fr x = ...accumulate x with result from (fr (- x 1))
, este é o quase-útil função - embora não possamos usarfn
diretamente sobrex
, será útil em breve. Este não recursivofn
usa uma funçãofr
para calcular seu resultadofn fr = fr
,fr
É o ponto de correção defn
,fr
é o útil funciton, podemos usarfr
emx
conseguir nosso resultadoY fn = fr
,Y
retorna o ponto de correção de uma função,Y
transforma nossa função quase útilfn
em útilfr
Derivação
Y
(não incluída)Vou pular a derivação
Y
e ir para o entendimentoY
. O post de Mike Vainer tem muitos detalhes.A forma de
Y
Y
é definido como (no formato de cálculo lambda ):Se substituirmos a variável
s
à esquerda das funções, obteremosEntão, de fato, o resultado de
(Y f)
é o ponto de correção def
.Por que
(Y f)
funciona?Dependendo da assinatura de
f
,(Y f)
pode ser uma função de qualquer área, para simplificar, vamos assumir(Y f)
apenas um parâmetro, como nossa função fatorial.desde então
fn fr = fr
, continuamoso cálculo recursivo termina quando o mais interno
(fn fr 1)
é o caso base efn
não é usadofr
no cálculo.Olhando
Y
novamente:assim
Para mim, as partes mágicas dessa configuração são:
fn
efr
interdependem um do outro:fr
'quebra'fn
dentro, toda vez quefr
é usado para calcularx
, 'gera' ('elevadores'?)fn
e delega o cálculo a elefn
(passando por si mesmofr
ex
); por outro lado,fn
dependefr
e usafr
para calcular o resultado de um problema menorx-1
.fr
é usado para definirfn
(quandofn
usadofr
em suas operações), o realfr
ainda não está definido.fn
que define a lógica real dos negócios. Com base emfn
,Y
criafr
- uma função de auxiliar numa forma específica - para facilitar o cálculo parafn
em um recursiva maneira.Isso me ajudou a entender
Y
dessa maneira no momento, espero que ajude.BTW, eu também achei muito bom o livro Uma Introdução à Programação Funcional através do Cálculo Lambda , sou apenas parte dele e o fato de não ter conseguido entender o que aconteceu
Y
no livro me levou a este post.fonte
Aqui estão as respostas para as perguntas originais , compiladas a partir do artigo (que vale TOTALMENTE leitura) mencionado na resposta de Nicholas Mancuso , bem como outras respostas:
Um combinador Y é uma "funcional" (ou uma função de ordem superior - uma função que opera em outras funções) que usa um único argumento, que é uma função que não é recursiva, e retorna uma versão da função que é recursivo.
Um pouco recursivo =), mas com uma definição mais profunda:
Um combinador - é apenas uma expressão lambda sem variáveis livres.
Variável livre - é uma variável que não é uma variável vinculada.
Variável vinculada - variável que está contida no corpo de uma expressão lambda que possui esse nome de variável como um de seus argumentos.
Outra maneira de pensar sobre isso é que combinator é uma expressão lambda, na qual você pode substituir o nome de um combinator por sua definição em todos os lugares em que é encontrado e ter tudo ainda funcionando (você entrará em um loop infinito se o combinator conter referência a si próprio, dentro do corpo lambda).
O combinador Y é um combinador de ponto fixo.
O ponto fixo de uma função é um elemento do domínio da função que é mapeado para ela mesma pela função.
Ou seja,
c
é um ponto fixo da funçãof(x)
sef(c) = c
Isso significa
f(f(...f(c)...)) = fn(c) = c
Os exemplos abaixo assumem digitação forte + dinâmica :
Combinador Y preguiçoso (ordem normal):
Esta definição se aplica a idiomas com avaliação preguiçosa (também: adiada, chamada por necessidade) - estratégia de avaliação que atrasa a avaliação de uma expressão até que seu valor seja necessário.
O que isso significa é que, para uma determinada função
f
(que é uma função não recursiva), a função recursiva correspondente pode ser obtida primeiro computandoλx.f(x x)
e depois aplicando essa expressão lambda a si mesma.Combinador Y estrito (por ordem de aplicação):
Esta definição se aplica a idiomas com avaliação estrita (também: ansiosa, gananciosa) - estratégia de avaliação na qual uma expressão é avaliada assim que é vinculada a uma variável.
É o mesmo que o preguiçoso em sua natureza, apenas possui um
λ
invólucro extra para atrasar a avaliação do corpo do lambda. Fiz outra pergunta , um pouco relacionada a esse tópico.Roubadoemprestado da resposta por Chris Ammerman : o combinador em Y generaliza a recursão, abstraindo sua implementação e, assim, separando-a do trabalho real da função em questão.Mesmo que o combinador Y tenha algumas aplicações práticas, é principalmente um conceito teórico, cuja compreensão expandirá sua visão geral e provavelmente aumentará suas habilidades analíticas e de desenvolvedor.
Como afirma Mike Vanier : é possível definir um combinador Y em muitas linguagens estaticamente tipadas, mas (pelo menos nos exemplos que eu vi) essas definições geralmente requerem algum tipo de hackeamento não óbvio, porque o próprio combinador Y não ' t tem um tipo estático direto. Isso está além do escopo deste artigo, então não vou mencionar mais
E, como mencionado por Chris Ammerman : a maioria das linguagens procedurais possui digitação estática.
Então responda a este - não realmente.
fonte
O combinador y implementa recursão anônima. Então, ao invés de
você pode fazer
é claro, o combinador-y só funciona nos idiomas de chamada por nome. Se você quiser usar isso em qualquer linguagem normal de chamada por valor, precisará do combinador z relacionado (o combinador y divergirá / loop infinito).
fonte
Um combinador de ponto fixo (ou operador de ponto fixo) é uma função de ordem superior que calcula um ponto fixo de outras funções. Essa operação é relevante na teoria da linguagem de programação, pois permite a implementação de recursão na forma de uma regra de reescrita, sem suporte explícito do mecanismo de tempo de execução da linguagem. (src Wikipedia)
fonte
O operador this pode simplificar sua vida:
Então você evita a função extra:
Finalmente, você liga
fac(5)
.fonte
Acho que a melhor maneira de responder a isso é escolher um idioma, como JavaScript:
Agora, reescreva-o para que não use o nome da função dentro da função, mas ainda a chame recursivamente.
O único local em que o nome da função
factorial
deve ser visto é no site da chamada.Dica: você não pode usar nomes de funções, mas pode usar nomes de parâmetros.
Trabalhe o problema. Não procure. Depois de resolvê-lo, você entenderá o problema que o combinador-y resolve.
fonte