¿Qué es un combinador en Y? [cerrado]


392

Un Y-combinator es un concepto informático desde el lado "funcional" de las cosas. La mayoría de los programadores no saben mucho acerca de los combinadores, incluso si han oído hablar de ellos.

  • ¿Qué es un combinador en Y?
  • ¿Cómo funcionan los combinadores?
  • ¿Para qué son buenos?
  • ¿Son útiles en lenguajes de procedimiento?

12
Poco de un consejo, si usted está aprendiendo acerca de los lenguajes funcionales como yo, mejores combinadores de licencia hasta que se sienta cómodo con él, de lo contrario es un camino a la locura ...
Igor Zevaka

3
Tengo que sonreír al gravatar del editor de esta pregunta :) Enlace relacionado en el blog de Mads Torgensen
Benjol


1
Escribí un breve resumen compartiendo mi comprensión del Y Combinator: gist.github.com/houtianze/b274e4b975a28fe08aee681699c3f7d0 Le expliqué (a mi entender) cómo el "Y Combinator hace una función recursiva"
Ibic

1
¿Cómo es esta pregunta "demasiado amplia"?
Rei Miyasaka

Respuestas:


201

Si estás listo para una lectura larga, Mike Vanier tiene una gran explicación . En pocas palabras, le permite implementar la recursividad en un lenguaje que no necesariamente lo admite de forma nativa.


14
Sin embargo, es un poco más que un enlace; Es un enlace con un resumen muy breve . Se agradecería un resumen más extenso.
Martijn Pieters

2
Es solo un enlace PERO no puede ser mejor que esto. Esta respuesta merece (agregar 1 voto) sin la condición de caso base para salir; también conocido como recursión infinita.
Yavar

77
@Andre MacFie: No hice ningún comentario sobre el esfuerzo, hice comentarios sobre la calidad. En general, la política sobre Stack Overflow es que las respuestas deben ser independientes, con enlaces a más información.
Jørgen Fogh

1
@galdre tiene razón. Es un gran enlace, pero es solo un enlace. También se ha mencionado en otras 3 respuestas a continuación, pero solo como un documento de respaldo, ya que todas son buenas explicaciones por sí mismas. Esta respuesta tampoco intenta responder las preguntas del OP.
toraritte

290

Un combinador en Y es un "funcional" (una función que opera en otras funciones) que permite la recursividad, cuando no puede referirse a la función desde sí mismo. En la teoría de la informática, generaliza la recursión , abstrae su implementación y, por lo tanto, la separa del trabajo real de la función en cuestión. El beneficio de no necesitar un nombre en tiempo de compilación para la función recursiva es una especie de bonificación. =)

Esto es aplicable en idiomas que admiten funciones lambda . La naturaleza basada en expresiones de lambdas generalmente significa que no pueden referirse a sí mismas por su nombre. Y trabajar alrededor de esto al declarar la variable, al referirse a ella, luego asignarle la lambda, para completar el ciclo de autorreferencia, es frágil. La variable lambda se puede copiar y la variable original se puede reasignar, lo que rompe la autorreferencia.

Los combinadores Y son engorrosos de implementar, y a menudo de usar, en lenguajes de tipo estático (que a menudo son los lenguajes de procedimiento ), porque generalmente las restricciones de tipeo requieren la cantidad de argumentos para que la función en cuestión se conozca en tiempo de compilación. Esto significa que se debe escribir un combinador y para cualquier recuento de argumentos que uno necesite usar.

A continuación se muestra un ejemplo de cómo el uso y el funcionamiento de un Y-Combinator, en C #.

Usar un Y-combinador implica una forma "inusual" de construir una función recursiva. Primero debe escribir su función como un fragmento de código que llama a una función preexistente, en lugar de a sí misma:

// Factorial, if func does the same thing as this bit of code...
x == 0 ? 1: x * func(x - 1);

Luego lo convierte en una función que toma una función para llamar y devuelve una función que lo hace. Esto se llama funcional, porque toma una función y realiza una operación con ella que da como resultado otra función.

// A function that creates a factorial, but only if you pass in
// a function that does what the inner function is doing.
Func<Func<Double, Double>, Func<Double, Double>> fact =
  (recurs) =>
    (x) =>
      x == 0 ? 1 : x * recurs(x - 1);

Ahora tiene una función que toma una función y devuelve otra función que parece un factorial, pero en lugar de llamarse a sí misma, llama al argumento pasado a la función externa. ¿Cómo haces que esto sea factorial? Pase la función interna a sí mismo. El Y-Combinator hace eso, al ser una función con un nombre permanente, que puede introducir la recursividad.

// One-argument Y-Combinator.
public static Func<T, TResult> Y<T, TResult>(Func<Func<T, TResult>, Func<T, TResult>> F)
{
  return
    t =>  // A function that...
      F(  // Calls the factorial creator, passing in...
        Y(F)  // The result of this same Y-combinator function call...
              // (Here is where the recursion is introduced.)
        )
      (t); // And passes the argument into the work function.
}

En lugar de que el factorial se llame a sí mismo, lo que sucede es que el factorial llama al generador factorial (devuelto por la llamada recursiva a Y-Combinator). Y dependiendo del valor actual de t, la función devuelta por el generador llamará al generador nuevamente, con t - 1, o simplemente devolverá 1, terminando la recursión.

Es complicado y críptico, pero todo se agita en tiempo de ejecución, y la clave de su funcionamiento es la "ejecución diferida" y la ruptura de la recursión para abarcar dos funciones. La F interna se pasa como un argumento , que se llamará en la próxima iteración, solo si es necesario .


55
¡Por qué, oh, por qué tenías que llamarlo 'Y' y el parámetro 'F'! ¡Simplemente se pierden en los argumentos de tipo!
Brian Henk

3
En Haskell, puede abstraer la recursividad con: fix :: (a -> a) -> ay, aa su vez, puede ser función de tantos argumentos como desee. Esto significa que la escritura estática no hace que esto sea engorroso.
Peaker

12
Según la descripción de Mike Vanier, su definición de Y en realidad no es un combinador porque es recursiva. En "Eliminar (la mayoría) de la recursión explícita (versión diferida)", tiene el equivalente del esquema diferido de su código C #, pero explica en el punto 2: "No es un combinador, porque la Y en el cuerpo de la definición es una variable libre que solo se vincula una vez que se completa la definición ... "Creo que lo bueno de los combinadores Y es que producen recursividad al evaluar el punto fijo de una función. De esta manera, no necesitan una recursión explícita.
GrantJ

@GrantJ Haces un buen punto. Han pasado un par de años desde que publiqué esta respuesta. Ojeando la publicación de Vanier ahora veo que he escrito Y, pero no un Y-Combinator. Leeré su publicación nuevamente pronto y veré si puedo publicar una corrección. Mi instinto me advierte que el estricto tipeo estático de C # puede evitarlo al final, pero veré qué puedo hacer.
Chris Ammerman

1
@WayneBurkett Es una práctica bastante común en matemáticas.
YoTengoUnLCD

102

He levantado esto de http://www.mail-archive.com/boston-pm@mail.pm.org/msg02716.html, que es una explicación que escribí hace varios años.

Usaré JavaScript en este ejemplo, pero muchos otros idiomas también funcionarán.

Nuestro objetivo es poder escribir una función recursiva de 1 variable usando solo funciones de 1 variables y sin asignaciones, definiendo cosas por nombre, etc. (Por qué este es nuestro objetivo es otra pregunta, tomemos esto como el desafío que tenemos son dados.) Parece imposible, ¿eh? Como ejemplo, implementemos factorial.

Bueno, el primer paso es decir que podríamos hacer esto fácilmente si engañamos un poco. Usando funciones de 2 variables y asignaciones, al menos podemos evitar tener que usar asignaciones para configurar la recursividad.

// Here's the function that we want to recurse.
X = function (recurse, n) {
  if (0 == n)
    return 1;
  else
    return n * recurse(recurse, n - 1);
};

// This will get X to recurse.
Y = function (builder, n) {
  return builder(builder, n);
};

// Here it is in action.
Y(
  X,
  5
);

Ahora veamos si podemos hacer menos trampa. Bueno, en primer lugar estamos usando la asignación, pero no necesitamos hacerlo. Podemos escribir X e Y en línea.

// No assignment this time.
function (builder, n) {
  return builder(builder, n);
}(
  function (recurse, n) {
    if (0 == n)
      return 1;
    else
      return n * recurse(recurse, n - 1);
  },
  5
);

Pero estamos usando funciones de 2 variables para obtener una función de 1 variable. ¿Podemos arreglar eso? Bueno, un tipo inteligente llamado Haskell Curry tiene un buen truco, si tienes buenas funciones de orden superior, solo necesitas funciones de 1 variable. La prueba es que puede pasar de funciones de 2 (o más en el caso general) a 1 variable con una transformación de texto puramente mecánica como esta:

// Original
F = function (i, j) {
  ...
};
F(i,j);

// Transformed
F = function (i) { return function (j) {
  ...
}};
F(i)(j);

donde ... permanece exactamente igual. (Este truco se llama "curry" por su inventor. El idioma Haskell también se llama así por Haskell Curry. Archívelo bajo trivia inútil). Ahora simplemente aplique esta transformación en todas partes y obtengamos nuestra versión final.

// The dreaded Y-combinator in action!
function (builder) { return function (n) {
  return builder(builder)(n);
}}(
  function (recurse) { return function (n) {
    if (0 == n)
      return 1;
    else
      return n * recurse(recurse)(n - 1);
  }})(
  5
);

Siéntase libre de probarlo. alertar () ese retorno, atarlo a un botón, lo que sea. Ese código calcula factoriales, recursivamente, sin usar asignación, declaraciones o funciones de 2 variables. (Pero tratar de rastrear cómo funciona es probable que haga que su cabeza gire. Y al entregarlo, sin la derivación, simplemente reformateado ligeramente dará como resultado un código que seguramente confundirá y confundirá).

Puede reemplazar las 4 líneas que definen recursivamente factorial con cualquier otra función recursiva que desee.


Buena explicación ¿Por qué escribiste en function (n) { return builder(builder)(n);}lugar de builder(builder)?
v7d8dpo4

@ v7d8dpo4 Porque estaba convirtiendo una función de 2 variables en una función de orden superior de una variable usando curry.
btilly

¿Es esta la razón por la que necesitamos cierres?
TheChetan

1
@TheChetan Closures nos permite vincular el comportamiento personalizado detrás de una llamada a una función anónima. Es solo otra técnica de abstracción.
btilly

85

Me pregunto si es útil intentar construir esto desde cero. Veamos. Aquí hay una función factorial recursiva básica:

function factorial(n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}

Refactoricemos y creemos una nueva función llamada factque devuelva una función anónima de cálculo factorial en lugar de realizar el cálculo en sí:

function fact() {
    return function(n) {
        return n == 0 ? 1 : n * fact()(n - 1);
    };
}

var factorial = fact();

Eso es un poco raro, pero no tiene nada de malo. Solo estamos generando una nueva función factorial en cada paso.

La recursividad en esta etapa todavía es bastante explícita. La factfunción debe ser consciente de su propio nombre. Vamos a parametrizar la llamada recursiva:

function fact(recurse) {
    return function(n) {
        return n == 0 ? 1 : n * recurse(n - 1);
    };
}

function recurser(x) {
    return fact(recurser)(x);
}

var factorial = fact(recurser);

Eso es genial, pero recurseraún necesita saber su propio nombre. Vamos a parametrizar eso también:

function recurser(f) {
    return fact(function(x) {
        return f(f)(x);
    });
}

var factorial = recurser(recurser);

Ahora, en lugar de llamar recurser(recurser)directamente, creemos una función de contenedor que devuelva su resultado:

function Y() {
    return (function(f) {
        return f(f);
    })(recurser);
}

var factorial = Y();

Ahora podemos deshacernos del recursernombre por completo; es solo un argumento para la función interna de Y, que puede reemplazarse con la función en sí:

function Y() {
    return (function(f) {
        return f(f);
    })(function(f) {
        return fact(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y();

El único nombre externo al que todavía se hace referencia es fact, pero ahora debería estar claro que también se puede parametrizar fácilmente, creando la solución completa y genérica:

function Y(le) {
    return (function(f) {
        return f(f);
    })(function(f) {
        return le(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y(function(recurse) {
    return function(n) {
        return n == 0 ? 1 : n * recurse(n - 1);
    };
});

Una explicación similar en JavaScript: igstan.ro/posts/...
Pops

1
Me perdiste cuando introdujiste la función recurser. No es la menor idea de lo que está haciendo o por qué.
Mörre

2
Estamos tratando de construir una solución recursiva genérica para funciones que no son explícitamente recursivas. La recurserfunción es el primer paso hacia este objetivo, porque nos da una versión recursiva de la factque nunca se hace referencia por su nombre.
Wayne

@WayneBurkett, ¿puedo volver a escribir el combinador Y como esto: function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });. Y así es como lo digiero (no estoy seguro si es correcto): al no hacer referencia explícita a la función (no permitida como combinador ), podemos usar dos funciones parcialmente aplicadas / currificadas (una función creadora y la función de cálculo), con ¿Qué podemos crear funciones lambda / anónimas que logren recursivo sin necesidad de un nombre para la función de cálculo?
neevek

50

La mayoría de las respuestas anteriores describen lo que el Y-Combinator es , pero no lo es para .

Los combinadores de punto fijo se utilizan para mostrar que el cálculo lambda se está completando . Este es un resultado muy importante en la teoría de la computación y proporciona una base teórica para la programación funcional .

Estudiar los combinadores de punto fijo también me ha ayudado a comprender realmente la programación funcional. Sin embargo, nunca he encontrado ningún uso para ellos en la programación real.


24

y-combinator en JavaScript :

var Y = function(f) {
  return (function(g) {
    return g(g);
  })(function(h) {
    return function() {
      return f(h(h)).apply(null, arguments);
    };
  });
};

var factorial = Y(function(recurse) {
  return function(x) {
    return x == 0 ? 1 : x * recurse(x-1);
  };
});

factorial(5)  // -> 120

Editar : aprendo mucho mirando el código, pero este es un poco difícil de tragar sin algunos antecedentes, lo siento. Con algunos conocimientos generales presentados por otras respuestas, puede comenzar a separar lo que está sucediendo.

La función Y es el "combinador y". Ahora eche un vistazo a la var factoriallínea donde se usa Y. Observe que le pasa una función que tiene un parámetro (en este ejemplo recurse) que también se usa más adelante en la función interna. El nombre del parámetro se convierte básicamente en el nombre de la función interna que le permite realizar una llamada recursiva (ya que utiliza recurse()en su definición). El combinador y realiza la magia de asociar la función interna anónima con el nombre del parámetro de la función pasada a Y.

Para la explicación completa de cómo Y hace la magia, revisó el artículo vinculado (no por mí, por cierto).


66
Javascript no necesita un Y-combinador para hacer una recursión anónima porque puede acceder a la función actual con argumentos.callee (ver en.wikipedia.org/wiki/… )
xitrium

66
arguments.calleeno está permitido en modo estricto: developer.mozilla.org/en/JavaScript/…
dave1010

2
Todavía puede asignarle un nombre a cualquier función, y si es la expresión de la función, ese nombre solo se conoce dentro de la función misma. (function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
Esailija


18

Para los programadores que no se han encontrado con la programación funcional en profundidad, y no les importa comenzar ahora, pero son un poco curiosos:

El combinador Y es una fórmula que le permite implementar la recursividad en una situación en la que las funciones no pueden tener nombres pero pueden pasarse como argumentos, usarse como valores de retorno y definirse dentro de otras funciones.

Funciona pasándose la función a sí misma como argumento, para que pueda llamarse a sí misma.

Es parte del cálculo lambda, que en realidad es matemática pero es efectivamente un lenguaje de programación, y es bastante fundamental para la informática y especialmente para la programación funcional.

El valor práctico diario del combinador Y es limitado, ya que los lenguajes de programación tienden a permitirle nombrar funciones.

En caso de que necesite identificarlo en una alineación policial, se ve así:

Y = λf. (Λx.f (xx)) (λx.f (xx))

Por lo general, puede detectarlo debido a la repetición (λx.f (x x)).

Los λsímbolos son la letra griega lambda, que le da su nombre al cálculo lambda, y hay muchos (λx.t)términos de estilo porque así es como se ve el cálculo lambda.


Esta debería ser la respuesta aceptada. Por cierto, con U x = x x, Y = U . (. U)(abusando de la notación similar a Haskell). OIA, con combinadores adecuados, Y = BU(CBU). Por lo tanto, Yf = U (f . U) = (f . U) (f . U) = f (U (f . U)) = f ((f . U) (f . U)).
Will Ness

13

Recursión anónima

Un combinador de punto fijo es una función de orden superior fixque, por definición, satisface la equivalencia.

forall f.  fix f  =  f (fix f)

fix frepresenta una solución xa la ecuación de punto fijo

               x  =  f x

El factorial de un número natural puede ser probado por

fact 0 = 1
fact n = n * fact (n - 1)

Utilizando fix, las pruebas constructivas arbitrarias sobre funciones generales / μ-recursivas se pueden derivar sin autorreferencialidad anónima.

fact n = (fix fact') n

dónde

fact' rec n = if n == 0
                then 1
                else n * rec (n - 1)

tal que

   fact 3
=  (fix fact') 3
=  fact' (fix fact') 3
=  if 3 == 0 then 1 else 3 * (fix fact') (3 - 1)
=  3 * (fix fact') 2
=  3 * fact' (fix fact') 2
=  3 * if 2 == 0 then 1 else 2 * (fix fact') (2 - 1)
=  3 * 2 * (fix fact') 1
=  3 * 2 * fact' (fix fact') 1
=  3 * 2 * if 1 == 0 then 1 else 1 * (fix fact') (1 - 1)
=  3 * 2 * 1 * (fix fact') 0
=  3 * 2 * 1 * fact' (fix fact') 0
=  3 * 2 * 1 * if 0 == 0 then 1 else 0 * (fix fact') (0 - 1)
=  3 * 2 * 1 * 1
=  6

Esta prueba formal de que

fact 3  =  6

utiliza metódicamente la equivalencia del combinador de punto fijo para reescrituras

fix fact'  ->  fact' (fix fact')

Cálculo lambda

El formalismo de cálculo lambda sin tipo consiste en una gramática libre de contexto

E ::= v        Variable
   |  λ v. E   Abstraction
   |  E E      Application

donde vrangos sobre variables, junto con las reglas de reducción beta y eta

(λ x. B) E  ->  B[x := E]                                 Beta
  λ x. E x  ->  E          if x doesn’t occur free in E   Eta

La reducción beta sustituye todas las ocurrencias libres de la variable xen el cuerpo de abstracción ("función") Bpor la expresión ("argumento") E. La reducción de Eta elimina la abstracción redundante. A veces se omite del formalismo. Una expresión irreducible , a la que no se aplica ninguna regla de reducción, está en forma normal o canónica .

λ x y. E

es taquigrafía para

λ x. λ y. E

(abstracción multiarity),

E F G

es taquigrafía para

(E F) G

(aplicación izquierda-asociatividad),

λ x. x

y

λ y. y

son alfa-equivalente .

La abstracción y la aplicación son las dos únicas "primitivas del lenguaje" del cálculo lambda, pero permiten la codificación de datos y operaciones arbitrariamente complejas.

Los números de la Iglesia son una codificación de los números naturales similares a los naturales Peano-axiomáticos.

   0  =  λ f x. x                 No application
   1  =  λ f x. f x               One application
   2  =  λ f x. f (f x)           Twofold
   3  =  λ f x. f (f (f x))       Threefold
    . . .

SUCC  =  λ n f x. f (n f x)       Successor
 ADD  =  λ n m f x. n f (m f x)   Addition
MULT  =  λ n m f x. n (m f) x     Multiplication
    . . .

Una prueba formal de que

1 + 2  =  3

usando la regla de reescritura de reducción beta:

   ADD                      1            2
=  (λ n m f x. n f (m f x)) (λ g y. g y) (λ h z. h (h z))
=  (λ m f x. (λ g y. g y) f (m f x)) (λ h z. h (h z))
=  (λ m f x. (λ y. f y) (m f x)) (λ h z. h (h z))
=  (λ m f x. f (m f x)) (λ h z. h (h z))
=  λ f x. f ((λ h z. h (h z)) f x)
=  λ f x. f ((λ z. f (f z)) x)
=  λ f x. f (f (f x))                                       Normal form
=  3

Combinadores

En el cálculo lambda, los combinadores son abstracciones que no contienen variables libres. Más simple: Iel combinador de identidad

λ x. x

isomorfo a la función de identidad

id x = x

Dichos combinadores son los operadores primitivos de los cálculos del combinador, como el sistema SKI.

S  =  λ x y z. x z (y z)
K  =  λ x y. x
I  =  λ x. x

La reducción beta no se normaliza fuertemente ; no todas las expresiones reducibles, "redexes", convergen a la forma normal bajo reducción beta. Un ejemplo simple es la aplicación divergente del ωcombinador omega

λ x. x x

a sí mismo:

   (λ x. x x) (λ y. y y)
=  (λ y. y y) (λ y. y y)
. . .
=  _|_                     Bottom

Se prioriza la reducción de subexpresiones más a la izquierda ("cabezas"). El orden de aplicación normaliza los argumentos antes de la sustitución, el orden normal no. Las dos estrategias son análogas a la evaluación entusiasta, por ejemplo, C, y la evaluación perezosa, por ejemplo, Haskell.

   K          (I a)        (ω ω)
=  (λ k l. k) ((λ i. i) a) ((λ x. x x) (λ y. y y))

diverge bajo la ansiosa reducción beta de orden aplicativo

=  (λ k l. k) a ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ y. y y) (λ y. y y))
. . .
=  _|_

ya que en semántica estricta

forall f.  f _|_  =  _|_

pero converge bajo una lenta reducción beta de orden normal

=  (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  a

Si una expresión tiene una forma normal, la reducción beta de orden normal la encontrará.

Y

La propiedad esencial del Y combinador de punto fijo.

λ f. (λ x. f (x x)) (λ x. f (x x))

es dado por

   Y g
=  (λ f. (λ x. f (x x)) (λ x. f (x x))) g
=  (λ x. g (x x)) (λ x. g (x x))           =  Y g
=  g ((λ x. g (x x)) (λ x. g (x x)))       =  g (Y g)
=  g (g ((λ x. g (x x)) (λ x. g (x x))))   =  g (g (Y g))
. . .                                      . . .

La equivalencia

Y g  =  g (Y g)

es isomorfo a

fix f  =  f (fix f)

El cálculo lambda sin tipo puede codificar pruebas constructivas arbitrarias sobre funciones generales / μ-recursivas.

 FACT  =  λ n. Y FACT' n
FACT'  =  λ rec n. if n == 0 then 1 else n * rec (n - 1)

   FACT 3
=  (λ n. Y FACT' n) 3
=  Y FACT' 3
=  FACT' (Y FACT') 3
=  if 3 == 0 then 1 else 3 * (Y FACT') (3 - 1)
=  3 * (Y FACT') (3 - 1)
=  3 * FACT' (Y FACT') 2
=  3 * if 2 == 0 then 1 else 2 * (Y FACT') (2 - 1)
=  3 * 2 * (Y FACT') 1
=  3 * 2 * FACT' (Y FACT') 1
=  3 * 2 * if 1 == 0 then 1 else 1 * (Y FACT') (1 - 1)
=  3 * 2 * 1 * (Y FACT') 0
=  3 * 2 * 1 * FACT' (Y FACT') 0
=  3 * 2 * 1 * if 0 == 0 then 1 else 0 * (Y FACT') (0 - 1)
=  3 * 2 * 1 * 1
=  6

(Multiplicación retrasada, confluencia)

Para el cálculo lambda sin tipo Churchian, se ha demostrado que existe una infinidad recursivamente enumerable de combinadores de punto fijo además Y.

 X  =  λ f. (λ x. x x) (λ x. f (x x))
Y'  =  (λ x y. x y x) (λ y x. y (x y x))
 Z  =  λ f. (λ x. f (λ v. x x v)) (λ x. f (λ v. x x v))
 Θ  =  (λ x y. y (x x y)) (λ x y. y (x x y))
  . . .

La reducción beta de orden normal hace que el cálculo lambda sin tipo no extendido sea un sistema de reescritura completo de Turing.

En Haskell, el combinador de punto fijo se puede implementar con elegancia

fix :: forall t. (t -> t) -> t
fix f = f (fix f)

La pereza de Haskell se normaliza a una finidad antes de que se hayan evaluado todas las subexpresiones.

primes :: Integral t => [t]
primes = sieve [2 ..]
   where
      sieve = fix (\ rec (p : ns) ->
                     p : rec [n | n <- ns
                                , n `rem` p /= 0])


44
Si bien aprecio la minuciosidad de la respuesta, de ninguna manera es accesible para un programador con pocos antecedentes matemáticos formales después del primer salto de línea.
Jared Smith

44
@ jared-smith La respuesta está destinada a contar una historia complementaria de Wonkaian sobre las nociones CS / matemáticas detrás del combinador Y. Creo que, probablemente, las mejores analogías posibles a conceptos familiares ya han sido extraídas por otros respondedores. Personalmente, siempre me ha gustado enfrentarme al verdadero origen, la radical novedad de una idea, a través de una bonita analogía. Las analogías más amplias me parecen inapropiadas y confusas.

1
Hola, combinador de identidad λ x . x, ¿cómo estás hoy?
MaiaVictor

Me gusta esta respuesta la mayor parte . ¡Simplemente borró todas mis preguntas!
Estudiante

11

Otras respuestas proporcionan una respuesta bastante concisa a esto, sin un hecho importante: no es necesario implementar un combinador de punto fijo en ningún lenguaje práctico de esta manera enrevesada y hacerlo no tiene ningún propósito práctico (excepto "mira, sé qué combinador en Y es"). Es un concepto teórico importante, pero de poco valor práctico.


6

Aquí hay una implementación de JavaScript del Y-Combinator y la función Factorial (del artículo de Douglas Crockford, disponible en: http://javascript.crockford.com/little.html ).

function Y(le) {
    return (function (f) {
        return f(f);
    }(function (f) {
        return le(function (x) {
            return f(f)(x);
        });
    }));
}

var factorial = Y(function (fac) {
    return function (n) {
        return n <= 2 ? n : n * fac(n - 1);
    };
});

var number120 = factorial(5);

6

Un Y-Combinator es otro nombre para un condensador de flujo.


44
muy divertido. :) los jóvenes (er) podrían no reconocer la referencia sin embargo.
Will Ness

2
¡jaja! Sí, el joven (yo) todavía puedo entender ...


Creo que esta respuesta podría ser especialmente confusa para quienes no hablan inglés. Uno podría dedicar bastante tiempo a comprender esta afirmación antes (o nunca) de darse cuenta de que es una referencia humorística de la cultura popular. (Me gusta, me sentiría mal si hubiera respondido esto y descubriera que alguien que está aprendiendo se desanimó)
Mike

5

He escrito una especie de "guía de idiotas" para el Y-Combinator tanto en Clojure como en Scheme para ayudarme a comprenderlo. Están influenciados por el material en "The Little Schemer"

En el esquema: https://gist.github.com/z5h/238891

o Clojure: https://gist.github.com/z5h/5102747

Ambos tutoriales son códigos intercalados con comentarios y deben ser cortados y pegados en su editor favorito.


5

Como novato en los combinadores, el artículo de Mike Vanier (gracias Nicholas Mancuso) me pareció realmente útil. Me gustaría escribir un resumen, además de documentar mi comprensión, si pudiera ser de ayuda para otros, estaría muy contento.

De asqueroso a menos asqueroso

Usando factorial como ejemplo, usamos la siguiente almost-factorialfunción para calcular el factorial de número x:

def almost-factorial f x = if iszero x
                           then 1
                           else * x (f (- x 1))

En el pseudocódigo anterior, almost-factorialtoma la función fy el número x( almost-factorialestá en curry, por lo que puede verse como tomar una función fy devolver una función de 1 aridad).

Cuando almost-factorialcalcula factorial para x, delega el cálculo de factorial para x - 1funcionar fy acumula ese resultado con x(en este caso, multiplica el resultado de (x - 1) con x).

Puede verse como almost-factorialtomas en una versión mala de la función factorial (que solo puede calcular hasta el número x - 1) y devuelve una versión menos mala de factorial (que calcula hasta el número x). Como en esta forma:

almost-factorial crappy-f = less-crappy-f

Si repetidamente pasamos la versión menos mala de factorial a almost-factorial, eventualmente obtendremos nuestra función factorial deseada f. Donde se puede considerar como:

almost-factorial f = f

Punto fijo

El hecho de que eso almost-factorial f = fsignifica fes el punto fijo de la función almost-factorial.

Esta fue una forma realmente interesante de ver las relaciones de las funciones anteriores y fue un momento aha para mí. (Lea la publicación de Mike sobre el punto fijo si no lo ha hecho)

Tres funciones

Para generalizar, tenemos un no-recursivo función fn(como nuestro casi-factorial), que tiene su punto fijo función fr(como nuestro f), entonces lo que Yhace es cuando das Y fn, Ydevuelve la función de corrección del punto de fn.

Entonces, en resumen (simplificado suponiendo que frsolo toma un parámetro; xdegenera a x - 1, x - 2... en recursión):

  • Definimos los cálculos básicos como fn: def fn fr x = ...accumulate x with result from (fr (- x 1))esta es la función casi útil , aunque no podemos usarla fndirectamente x, será útil muy pronto. Este no recursivo fnutiliza una función frpara calcular su resultado.
  • fn fr = fr, frEs el punto fijo de fn, fres la utilidad Funciton, podemos usar fren xconseguir nuestro resultado
  • Y fn = fr, YDevuelve el punto fijo de una función, Y dirige nuestra casi-útil función fnen la utilidad fr

Derivado Y(no incluido)

Me saltearé la derivación Ye iré a entender Y. La publicación de Mike Vainer tiene muchos detalles.

La forma de Y

Yse define como (en formato de cálculo lambda ):

Y f = λs.(f (s s)) λs.(f (s s))

Si reemplazamos la variable sa la izquierda de las funciones, obtenemos

Y f = λs.(f (s s)) λs.(f (s s))
=> f (λs.(f (s s)) λs.(f (s s)))
=> f (Y f)

De hecho, el resultado de (Y f)es el punto fijo de f.

¿Por qué (Y f)funciona?

Dependiendo de la firma de f, (Y f)puede ser una función de cualquier aridad, para simplificar, supongamos que (Y f)solo toma un parámetro, como nuestra función factorial.

def fn fr x = accumulate x (fr (- x 1))

desde entonces fn fr = fr, continuamos

=> accumulate x (fn fr (- x 1))
=> accumulate x (accumulate (- x 1) (fr (- x 2)))
=> accumulate x (accumulate (- x 1) (accumulate (- x 2) ... (fn fr 1)))

el cálculo recursivo termina cuando el más interno (fn fr 1)es el caso base y fnno se usa fren el cálculo.

Mirando de Ynuevo:

fr = Y fn = λs.(fn (s s)) λs.(fn (s s))
=> fn (λs.(fn (s s)) λs.(fn (s s)))

Entonces

fr x = Y fn x = fn (λs.(fn (s s)) λs.(fn (s s))) x

Para mí, las partes mágicas de esta configuración son:

  • fne frinterdependen unos de otros: fr'envuelve' fndentro, cada vez que frse usa para calcular x, 'genera' ('levanta') fny delega el cálculo en eso fn(pasando en sí mismo fry x); por otro lado, fndepende fry se usa frpara calcular el resultado de un problema menor x-1.
  • En el momento frse usa para definir fn(cuando se fnusa fren sus operaciones), lo real fraún no está definido.
  • Es lo fnque define la lógica comercial real. Sobre la base de fn, Ycrea fr- una función auxiliar en una forma específica - para facilitar el cálculo para fnen un recursivo manera.

Me ayudó a entender de Yesta manera en este momento, espero que ayude.

Por cierto, también encontré el libro Una introducción a la programación funcional a través del cálculo Lambda muy bueno, solo estoy en parte y el hecho de que no podía entender Yel libro me llevó a esta publicación.


5

Aquí hay respuestas a las preguntas originales , compiladas del artículo (que vale la pena leer) mencionado en la respuesta de Nicholas Mancuso , así como otras respuestas:

¿Qué es un combinador en Y?

Un Y-combinador es un "funcional" (o una función de orden superior, una función que opera en otras funciones) que toma un solo argumento, que es una función que no es recursiva, y devuelve una versión de la función que es recursivo


Algo recursivo =), pero una definición más profunda:

Un combinador: es solo una expresión lambda sin variables libres.
Variable libre: es una variable que no es una variable enlazada.
Variable enlazada: variable contenida dentro del cuerpo de una expresión lambda que tiene ese nombre de variable como uno de sus argumentos.

Otra forma de pensar en esto es que el combinador es una expresión lambda, en la que puede reemplazar el nombre de un combinador con su definición en todos los lugares donde se encuentre y hacer que todo funcione (entrará en un bucle infinito si combinador contiene referencia a sí mismo, dentro del cuerpo lambda).

Y-combinator es un combinador de punto fijo.

El punto fijo de una función es un elemento del dominio de la función que se asigna a sí mismo por la función.
Es decir, ces un punto fijo de la función f(x)si f(c) = c
esto significaf(f(...f(c)...)) = fn(c) = c

¿Cómo funcionan los combinadores?

Los siguientes ejemplos suponen una tipificación fuerte + dinámica :

Combinador Y perezoso (orden normal):
esta definición se aplica a los lenguajes con evaluación perezosa (también: diferida, llamada por necesidad): estrategia de evaluación que retrasa la evaluación de una expresión hasta que se necesite su valor.

Y = λf.(λx.f(x x)) (λx.f(x x)) = λf.(λx.(x x)) (λx.f(x x))

Lo que esto significa es que, para una función dada f(que es una función no recursiva), la función recursiva correspondiente se puede obtener primero computando λx.f(x x)y luego aplicando esta expresión lambda a sí misma.

Combinador Y estricto (orden aplicativo):
esta definición se aplica a los lenguajes con una evaluación estricta (también: ansiosa, codiciosa): estrategia de evaluación en la que una expresión se evalúa tan pronto como está vinculada a una variable.

Y = λf.(λx.f(λy.((x x) y))) (λx.f(λy.((x x) y))) = λf.(λx.(x x)) (λx.f(λy.((x x) y)))

Es igual que el perezoso en su naturaleza, solo tiene λenvoltorios adicionales para retrasar la evaluación del cuerpo del lambda. He hecho otra pregunta , algo relacionada con este tema.

¿Para qué son buenos?

Robado prestado de la respuesta de Chris Ammerman : el combinador en Y generaliza la recursión, abstrae su implementación y, por lo tanto, la separa del trabajo real de la función en cuestión.

A pesar de que Y-combinator tiene algunas aplicaciones prácticas, es principalmente un concepto teórico, cuya comprensión ampliará su visión general y, probablemente, aumentará sus habilidades analíticas y de desarrollo.

¿Son útiles en lenguajes de procedimiento?

Como dijo Mike Vanier : es posible definir un combinador Y en muchos lenguajes estáticamente tipados, pero (al menos en los ejemplos que he visto) tales definiciones generalmente requieren algún tipo de piratería no obvia, porque el combinador Y en sí mismo no Tiene un tipo estático directo. Eso está más allá del alcance de este artículo, por lo que no lo mencionaré más

Y como lo mencionó Chris Ammerman : la mayoría de los lenguajes de procedimiento tienen tipeo estático.

Entonces responda a esta, no realmente.


4

El combinador y implementa la recursión anónima. Entonces en lugar de

function fib( n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

tu puedes hacer

function ( fib, n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

por supuesto, el combinador y solo funciona en lenguajes de llamada por nombre. Si desea utilizar esto en cualquier lenguaje normal de llamada por valor, necesitará el combinador z relacionado (el combinador y divergerá / bucle infinito).


El combinador Y puede funcionar con evaluación de valor por paso y perezosa.
Quelklef

3

Un combinador de punto fijo (u operador de punto fijo) es una función de orden superior que calcula un punto fijo de otras funciones. Esta operación es relevante en la teoría del lenguaje de programación porque permite la implementación de la recursión en forma de una regla de reescritura, sin el soporte explícito del motor de tiempo de ejecución del lenguaje. (src Wikipedia)


3

El operador this puede simplificar su vida:

var Y = function(f) {
    return (function(g) {
        return g(g);
    })(function(h) {
        return function() {
            return f.apply(h(h), arguments);
        };
    });
};

Entonces evitas la función extra:

var fac = Y(function(n) {
    return n == 0 ? 1 : n * this(n - 1);
});

Finalmente llamas fac(5).


0

Creo que la mejor manera de responder esto es elegir un idioma, como JavaScript:

function factorial(num)
{
    // If the number is less than 0, reject it.
    if (num < 0) {
        return -1;
    }
    // If the number is 0, its factorial is 1.
    else if (num == 0) {
        return 1;
    }
    // Otherwise, call this recursive procedure again.
    else {
        return (num * factorial(num - 1));
    }
}

Ahora vuelva a escribirlo para que no use el nombre de la función dentro de la función, pero aún así lo llame de forma recursiva.

El único lugar donde factorialdebe verse el nombre de la función es en el sitio de la llamada.

Sugerencia: no puede usar nombres de funciones, pero puede usar nombres de parámetros.

Trabaja el problema. No lo busques. Una vez que lo resuelva, comprenderá qué problema resuelve el combinador y.


1
¿Estás seguro de que no crea más problemas de los que resuelve?
Noctis Skytower

1
Noctis, ¿puedes aclarar tu pregunta? ¿Está preguntando si el concepto de un combinador en sí crea más problemas de los que resuelve, o está hablando específicamente de que elegí demostrar usando JavaScript en particular, o mi implementación específica o mi recomendación para aprenderlo descubriéndolo usted mismo como ¿Yo describí?
zumalifeguard
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.