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

Resuelto Chris Ammerman asked hace 15 años • 18 respuestas

Un combinador en Y es un concepto informático desde el lado “funcional” de las cosas. La mayoría de los programadores no saben mucho sobre combinadores, si es que 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 procesales?
Chris Ammerman avatar Sep 18 '08 22:09 Chris Ammerman
Aceptado

Un combinador Y es un "funcional" (una función que opera sobre otras funciones) que permite la recursividad, cuando no se puede hacer referencia a la función desde dentro de sí misma. En la teoría de la informática, generaliza la recursión , abstrayendo su implementación y separándola así 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 ventaja. =)

Esto es aplicable en lenguajes que admiten funciones lambda . La naturaleza basada en expresiones de las lambdas generalmente significa que no pueden referirse a sí mismas por su nombre. Y solucionar esto declarando la variable, haciendo referencia a ella y luego asignándole la lambda, para completar el ciclo de autorreferencia, es frágil. La variable lambda se puede copiar y reasignar la variable original, lo que rompe la autorreferencia.

Los combinadores Y son engorrosos de implementar, y a menudo de usar, en lenguajes de tipo estático (que suelen ser los lenguajes de procedimiento ), porque normalmente las restricciones de tipeo requieren que se conozca el número de argumentos para la función en cuestión en el momento de la compilación. Esto significa que se debe escribir un combinador y para cualquier recuento de argumentos que sea necesario utilizar.

A continuación se muestra un ejemplo de cómo se usa y funciona un Y-Combinator en C#.

El uso de un combinador en Y implica una forma "inusual" de construir una función recursiva. Primero debes escribir tu función como un fragmento de código que llame a una función preexistente, en lugar de llamarla a sí misma:

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

Luego lo conviertes 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 se hace que esto sea el 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 recursividad.

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

Chris Ammerman avatar Sep 18 '2008 16:09 Chris Ammerman

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

Nicholas Mancuso avatar Sep 18 '2008 15:09 Nicholas Mancuso

Saqué esto de http://www.mail-archive.com/[email protected] /msg02716.html , que es una explicación que escribí hace varios años.

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

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

Bueno, el paso 1 es decir que podríamos hacer esto fácilmente si hiciéramos un poco de trampa. Usando funciones de 2 variables y asignación, al menos podemos evitar tener que usar la asignación 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 trampas. Bueno, en primer lugar estamos usando la asignación, pero no es necesario. Podemos simplemente 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, entonces solo necesitas funciones de 1 variable. La prueba es que puedes pasar de funciones de 2 (o más en el caso general) variables 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... sigue siendo exactamente igual. (Este truco se llama "curry" en honor a su inventor. El lenguaje Haskell también lleva el nombre de Haskell Curry. Archívelo en trivia inútil). Ahora simplemente aplique esta transformación en todas partes y obtendremos 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
);

No dudes en probarlo. alert() ese retorno, átelo a un botón, lo que sea. Ese código calcula factoriales, de forma recursiva, sin utilizar asignaciones, declaraciones o funciones de 2 variables. (Pero tratar de rastrear cómo funciona probablemente le haga girar la cabeza. Y entregarlo, sin la derivación, ligeramente reformateado dará como resultado un código que seguramente desconcertará y confundirá).

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

btilly avatar Jul 15 '2011 21:07 btilly

Me pregunto si tiene alguna utilidad intentar construir esto desde cero. Vamos a ver. 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 de cálculo factorial anónima 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();

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

La recursividad en esta etapa sigue siendo bastante explícita. La factfunción debe conocer su propio nombre. Parametricemos 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. Parametricémoslo 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 contenedora 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 misma:

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 aún se hace referencia es fact, pero ya debería quedar claro que también se puede parametrizar fácilmente, creando una 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);
    };
});
Wayne avatar Jul 15 '2011 23:07 Wayne