¿Es esta una función pura?

117

La mayoría de las fuentes definen una función pura que tiene las siguientes dos propiedades:

  1. Su valor de retorno es el mismo para los mismos argumentos.
  2. Su evaluación no tiene efectos secundarios.

Es la primera condición que me preocupa. En la mayoría de los casos, es fácil juzgar. Considere las siguientes funciones de JavaScript (como se muestra en este artículo )

Puro:

const add = (x, y) => x + y;

add(2, 4); // 6

Impuro:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

Es fácil ver que la segunda función dará diferentes salidas para llamadas posteriores, violando así la primera condición. Y por lo tanto, es impuro.

Esta parte la tengo.


Ahora, para mi pregunta, considere esta función que convierte una cantidad dada en dólares a euros:

(EDITAR - Utilizando consten la primera línea. Utilizado letanteriormente sin darse cuenta).

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Supongamos que buscamos el tipo de cambio de un db y cambia todos los días.

Ahora, no importa cuántas veces llame a esta función hoy , me dará la misma salida para la entrada 100. Sin embargo, podría darme una salida diferente mañana. No estoy seguro de si esto viola la primera condición o no.

IOW, la función en sí no contiene ninguna lógica para mutar la entrada, pero se basa en una constante externa que podría cambiar en el futuro. En este caso, es absolutamente seguro que cambiará a diario. En otros casos, puede suceder; Puede que no.

¿Podemos llamar a tales funciones funciones puras? Si la respuesta es NO, ¿cómo podemos refactorizarlo para que sea uno?

Monigote de nieve
fuente
66
Pureza de un lenguaje tan dinámico como JS es un tema muy complicado:function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
zerkms
29
La pureza significa que puede reemplazar la llamada de función con su valor de resultado a nivel de código sin cambiar el comportamiento de su programa.
bob
1
Para ir un poco más allá sobre lo que constituye un efecto secundario, y con una terminología más teórica, vea cs.stackexchange.com/questions/116377/…
Gilles 'SO- deja de ser malo'
3
Hoy, la función es (x) => {return x * 0.9;}. Mañana, tendrá una función diferente que también será pura, tal vez (x) => {return x * 0.89;}. Tenga en cuenta que cada vez que lo ejecuta (x) => {return x * exchangeRate;}crea una nueva función, y esa función es pura porque exchangeRateno puede cambiar.
user253751
2
Esta es una función impura. Si desea que sea pura, puede usarla const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; para una función pura, Its return value is the same for the same arguments.debe mantenerse siempre, 1 segundo, 1 década ... más tarde
pase

Respuestas:

133

El dollarToEurovalor de retorno de 's depende de una variable externa que no es un argumento; por lo tanto, la función es impura.

En la respuesta es NO, ¿cómo podemos refactorizar la función para que sea pura?

Una opción es pasar exchangeRate. De esta manera, cada vez que se presentan argumentos (something, somethingElse), se garantiza que la salida sea something * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Tenga en cuenta que para la programación funcional, debe evitar let, siempre use constpara evitar la reasignación.

Cierto rendimiento
fuente
66
No tener variables libres no es un requisito para una función a ser puro: const add = x => y => x + y; const one = add(42);Aquí tanto addy oneson funciones puras.
zerkms
77
const foo = 42; const add42 = x => x + foo;<- esta es otra función pura, que nuevamente utiliza variables libres.
zerkms
8
@zerkms: me gustaría mucho ver su respuesta a esta pregunta (incluso si solo modifica las palabras de CertainPerformance para usar una terminología diferente). No creo que se esté duplicando, y sería esclarecedor, particularmente cuando se cita (idealmente con mejores fuentes que el artículo de Wikipedia anterior, pero si eso es todo lo que obtenemos, sigue siendo una victoria). (Sería fácil leer este comentario con algún tipo de luz negativa. Confía en mí para ser sincero, creo que tal respuesta sería excelente y me gustaría leerla.)
TJ Crowder
17
Creo que tanto usted como @zerkms están equivocados. Parece pensar que la dollarToEurofunción del ejemplo en su respuesta es impura porque depende de la variable libre exchangeRate. Eso es absurdo. Como señaló zerkms, la pureza de una función no tiene nada que ver con si tiene o no variables libres. Sin embargo, zerkms también está equivocado porque cree que la dollarToEurofunción es impura porque depende de la exchangeRateque proviene de una base de datos. Él dice que es impuro porque "depende de la IO transitivamente".
Aadit M Shah
99
(cont.) De nuevo, eso es absurdo porque sugiere que dollarToEuroes impuro porque exchangeRatees una variable libre. Sugiere que si exchangeRateno fuera una variable libre, es decir, si fuera un argumento, entonces dollarToEurosería puro. Por lo tanto, sugiere que dollarToEuro(100)es impuro pero dollarToEuro(100, exchangeRate)es puro. Eso es claramente absurdo porque en ambos casos dependes del exchangeRateque proviene de una base de datos. La única diferencia es si es o no exchangeRateuna variable libre dentro de la dollarToEurofunción.
Aadit M Shah
76

Técnicamente, cualquier programa que ejecute en una computadora es impuro porque eventualmente se compila en instrucciones como "mover este valor a eax" y "agregar este valor al contenido de eax", que son impuros. Eso no es muy útil.

En cambio, pensamos en la pureza usando cajas negras . Si algún código siempre produce las mismas salidas cuando se le dan las mismas entradas, entonces se considera puro. Según esta definición, la siguiente función también es pura, aunque internamente utiliza una tabla de notas impura.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

No nos interesan las partes internas porque estamos utilizando una metodología de caja negra para verificar la pureza. Del mismo modo, no nos importa que todo el código se convierta finalmente en instrucciones de máquina impuras porque estamos pensando en la pureza utilizando una metodología de recuadro negro. Lo interno no es importante.

Ahora, considere la siguiente función.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

¿Es la greetfunción pura o impura? Según nuestra metodología de recuadro negro, si le damos la misma entrada (por ejemplo World), siempre imprime la misma salida en la pantalla (es decir Hello World!). En ese sentido, ¿no es puro? No, no es. La razón por la que no es pura es porque consideramos que imprimir algo en la pantalla es un efecto secundario. Si nuestra caja negra produce efectos secundarios, entonces no es pura.

¿Qué es un efecto secundario? Aquí es donde el concepto de transparencia referencial es útil. Si una función es referencialmente transparente, siempre podemos reemplazar las aplicaciones de esa función con sus resultados. Tenga en cuenta que esto no es lo mismo que la función en línea .

En la función en línea, reemplazamos las aplicaciones de una función con el cuerpo de la función sin alterar la semántica del programa. Sin embargo, una función referencialmente transparente siempre se puede reemplazar con su valor de retorno sin alterar la semántica del programa. Considere el siguiente ejemplo.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

Aquí, subrayamos la definición greety no cambió la semántica del programa.

Ahora, considere el siguiente programa.

undefined;
undefined;

Aquí, reemplazamos las aplicaciones de la greetfunción con sus valores de retorno y cambió la semántica del programa. Ya no estamos imprimiendo saludos a la pantalla. Esa es la razón por la cual la impresión se considera un efecto secundario, y es por eso que la greetfunción es impura. No es referencialmente transparente.

Ahora, consideremos otro ejemplo. Considere el siguiente programa.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

Claramente, la mainfunción es impura. Sin embargo, es eltimeDiff función pura o impura? Aunque depende de serverTimecuál provenga de una llamada de red impura, sigue siendo referencialmente transparente porque devuelve las mismas salidas para las mismas entradas y porque no tiene ningún efecto secundario.

zerkms probablemente no estará de acuerdo conmigo en este punto. En su respuesta , dijo que eldollarToEuro función en el siguiente ejemplo es impura porque "depende de la IO transitivamente".

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Tengo que estar en desacuerdo con él porque el hecho de que exchangeRate provenga de una base de datos es irrelevante. Es un detalle interno y nuestra metodología de recuadro negro para determinar la pureza de una función no se preocupa por los detalles internos.

En lenguajes puramente funcionales como Haskell, tenemos una escotilla de escape para ejecutar efectos de E / S arbitrarios. Se llamaunsafePerformIO , y como su nombre lo indica, si no lo usa correctamente, entonces no es seguro porque podría romper la transparencia referencial. Sin embargo, si sabes lo que estás haciendo, entonces es perfectamente seguro de usar.

Generalmente se usa para cargar datos de archivos de configuración cerca del comienzo del programa. Cargar datos de archivos de configuración es una operación de IO impura. Sin embargo, no queremos ser agobiados al pasar los datos como entradas a cada función. Por lo tanto, si usamosunsafePerformIO , podemos cargar los datos en el nivel superior y todas nuestras funciones puras pueden depender de los datos de configuración global inmutables.

Tenga en cuenta que el hecho de que una función dependa de algunos datos cargados desde un archivo de configuración, una base de datos o una llamada de red, no significa que la función sea impura.

Sin embargo, consideremos su ejemplo original que tiene una semántica diferente.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Aquí, supongo que debido a que exchangeRateno está definido como const, se modificará mientras se ejecuta el programa. Si ese es el caso, entonces dollarToEurodefinitivamente es una función impura porque cuando exchangeRatese modifica, se romperá la transparencia referencial.

Sin embargo, si la exchangeRatevariable no se modifica y nunca se modificará en el futuro (es decir, si es un valor constante), aunque se defina como let, no se romperá la transparencia referencial. En ese caso, dollarToEuroes de hecho una función pura.

Tenga en cuenta que el valor de exchangeRatepuede cambiar cada vez que ejecute el programa nuevamente y no romperá la transparencia referencial. Solo rompe la transparencia referencial si cambia mientras el programa se está ejecutando.

Por ejemplo, si ejecuta mi timeDiffejemplo varias veces, obtendrá diferentes valores serverTimey, por lo tanto, diferentes resultados. Sin embargo, debido a que el valor de serverTimenunca cambia mientras se ejecuta el programa, la timeDifffunción es pura.

Aadit M Shah
fuente
3
Esto fue muy informativo. Gracias. Y quise usar consten mi ejemplo.
Muñeco de nieve el
3
Si quisiste usarlo, constentonces la dollarToEurofunción es pura. La única forma en que el valor de exchangeRatecambiaría sería si volviera a ejecutar el programa. En ese caso, el proceso anterior y el nuevo proceso son diferentes. Por lo tanto, no rompe la transparencia referencial. Es como llamar a una función dos veces con diferentes argumentos. Los argumentos pueden ser diferentes, pero dentro de la función el valor de los argumentos permanece constante.
Aadit M Shah
3
Esto suena como una pequeña teoría sobre la relatividad: las constantes son solo relativamente constantes, no absolutamente, es decir, relativas al proceso de ejecución. Claramente, la única respuesta correcta aquí. +1.
Bob
55
No estoy de acuerdo con "es impuro porque finalmente se compila en instrucciones como" mover este valor a eax "y" agregar este valor al contenido de eax " . Si eaxse borra, mediante una carga o un borrado, el código sigue siendo determinista independientemente de qué más está sucediendo y, por lo tanto, es puro. De lo contrario, una respuesta muy completa.
3Dave
3
@Bergi: En realidad, en un lenguaje puro con valores inmutables, la identidad es irrelevante. Si dos referencias que evalúan el mismo valor son dos referencias al mismo objeto o a diferentes objetos solo se puede observar mutando el objeto a través de una de las referencias y observando si el valor también cambia cuando se recupera a través de la otra referencia. Sin mutación, la identidad se vuelve irrelevante. (Como diría Rich Hickey: La identidad es una serie de Estados a lo largo del tiempo.)
Jörg W Mittag
23

Una respuesta de un purista de mí (donde "yo" es literalmente yo, ya que creo que esta pregunta no tiene un solo formal respuesta "correcta" ):

En un lenguaje tan dinámico como JS, con tantas posibilidades para crear tipos básicos de parches o inventar tipos personalizados utilizando características como Object.prototype.valueOf es imposible saber si una función es pura con solo mirarla, ya que depende de quien llama si quieren para producir efectos secundarios.

Una demostración:

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

Una respuesta de mí-pragmatista:

Desde la definición misma de wikipedia

En la programación de computadoras, una función pura es una función que tiene las siguientes propiedades:

  1. Su valor de retorno es el mismo para los mismos argumentos (sin variación con variables estáticas locales, variables no locales, argumentos de referencia mutables o flujos de entrada desde dispositivos de E / S).
  2. Su evaluación no tiene efectos secundarios (sin mutación de variables estáticas locales, variables no locales, argumentos de referencia mutables o flujos de E / S).

En otras palabras, solo importa cómo se comporta una función, no cómo se implementa. Y siempre que una función particular tenga estas 2 propiedades, es pura independientemente de cómo se implementó exactamente.

Ahora a su función:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Es impuro porque no califica el requisito 2: depende de la IO transitivamente.

Acepto que la declaración anterior es incorrecta, consulte la otra respuesta para obtener más detalles: https://stackoverflow.com/a/58749249/251311

Otros recursos relevantes:

zerkms
fuente
44
@TJCrowder mecomo zerkms que proporciona una respuesta.
zerkms
2
Sí, con Javascript se trata de confianza, no de garantías
Bob
44
@bob ... o es una llamada de bloqueo.
zerkms
1
@zerkms - Gracias. Solo para estar 100% seguro, la diferencia clave entre tu add42y mi addXes puramente que mi xpuede ser cambiado y tu ftno puede ser cambiado (y por lo tanto, add42el valor de retorno no varía según ft).
TJ Crowder
55
No estoy de acuerdo con que la dollarToEurofunción en su ejemplo sea impura. Le expliqué por qué no estoy de acuerdo con mi respuesta. stackoverflow.com/a/58749249/783743
Aadit M Shah
14

Como han dicho otras respuestas, la forma en que lo ha implementado dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

es realmente puro, porque el tipo de cambio no se actualiza mientras se ejecuta el programa. Conceptualmente, sin embargo, dollarToEuroparece que debería ser una función impura, ya que utiliza el tipo de cambio más actualizado. La forma más sencilla de explicar esta discrepancia es que no ha implementado dollarToEuropero dollarToEuroAtInstantOfProgramStart.

La clave aquí es que hay varios parámetros que se requieren para calcular una conversión de moneda, y que una versión verdaderamente pura del general dollarToEurolos proporcionaría a todos. Los parámetros más directos son la cantidad de USD para convertir y el tipo de cambio. Sin embargo, dado que desea obtener su tipo de cambio de la información publicada, ahora tiene tres parámetros que proporcionar:

  • La cantidad de dinero para intercambiar
  • Una autoridad histórica para consultar los tipos de cambio.
  • La fecha en que tuvo lugar la transacción (para indexar la autoridad histórica)

La autoridad histórica aquí es su base de datos, y suponiendo que la base de datos no esté comprometida, siempre devolverá el mismo resultado para el tipo de cambio en un día en particular. Por lo tanto, con la combinación de estos tres parámetros, puede escribir una versión del general completamente pura y autosuficiente dollarToEuro, que podría verse así:

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

Su implementación captura valores constantes tanto para la autoridad histórica como para la fecha de la transacción en el momento en que se crea la función: la autoridad histórica es su base de datos y la fecha capturada es la fecha en que inicia el programa; todo lo que queda es el monto en dólares , que proporciona la persona que llama. La versión impura de dollarToEuroeso siempre obtiene el valor más actualizado esencialmente toma el parámetro de fecha implícitamente, configurándolo en el instante en que se llama a la función, lo cual no es puro simplemente porque nunca se puede llamar a la función con los mismos parámetros dos veces.

Si desea tener una versión pura de dollarToEuroeso, aún puede obtener el valor más actualizado, aún puede vincular la autoridad histórica, pero dejar el parámetro de fecha sin consolidar y solicitar la fecha a la persona que llama como argumento, terminando con algo como esto:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());
TheHansinator
fuente
@Snowman De nada! Actualicé un poco la respuesta para agregar más ejemplos de código.
TheHansinator
8

Me gustaría retroceder un poco de los detalles específicos de JS y la abstracción de definiciones formales, y hablar sobre qué condiciones deben cumplirse para permitir optimizaciones específicas. Por lo general, eso es lo principal que nos importa al escribir código (aunque también ayuda a demostrar la corrección). La programación funcional no es una guía de las últimas modas ni un voto monástico de abnegación. Es una herramienta para resolver problemas.

Cuando tienes un código como este:

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Si exchangeRatenunca se pudo modificar entre las dos llamadas a dollarToEuro(100), es posible memorizar el resultado de la primera llamada dollarToEuro(100)y optimizar la segunda llamada. El resultado será el mismo, por lo que podemos recordar el valor de antes.

Se exchangeRatepuede configurar una vez, antes de llamar a cualquier función que lo busque, y nunca modificarlo. De manera menos restrictiva, es posible que tenga un código que busque una exchangeRatevez una función o bloque de código en particular, y use el mismo tipo de cambio de manera consistente dentro de ese alcance. O, si solo este hilo puede modificar la base de datos, tendrá derecho a asumir que, si no actualizó el tipo de cambio, nadie más lo ha cambiado en usted.

Si fetchFromDatabase() es en sí misma una función pura que se evalúa como una constante y exchangeRatees inmutable, podríamos doblar esta constante durante todo el cálculo. Un compilador que sepa que este es el caso podría hacer la misma deducción que hizo en el comentario, que se dollarToEuro(100)evalúa en 90.0, y reemplaza toda la expresión con la constante 90.0.

Sin embargo, si fetchFromDatabase()no realiza E / S, lo que se considera un efecto secundario, su nombre viola el Principio de Menos Asombro.

Davislor
fuente
8

Esta función no es pura, se basa en una variable externa, que casi definitivamente va a cambiar.

Por lo tanto, la función falla el primer punto que hizo, no devuelve el mismo valor cuando para los mismos argumentos.

Para hacer que esta función sea "pura", pase exchangeRatecomo argumento.

Esto satisfaría ambas condiciones.

  1. Siempre devolvería el mismo valor al pasar el mismo valor y tipo de cambio.
  2. Tampoco tendría efectos secundarios.

Código de ejemplo:

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())
Jessica
fuente
1
"lo que definitivamente va a cambiar" --- no lo es, lo es const.
zerkms
7

Para ampliar los puntos que otros han hecho sobre la transparencia referencial: podemos definir la pureza como simplemente ser la transparencia referencial de las llamadas a funciones (es decir, cada llamada a la función puede ser reemplazada por el valor de retorno sin cambiar la semántica del programa).

Las dos propiedades que otorga son ambas consecuencias de la transparencia referencial. Por ejemplo, la siguiente función f1es impura, ya que no da el mismo resultado cada vez (la propiedad que ha numerado 1):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

¿Por qué es importante obtener el mismo resultado cada vez? Porque obtener resultados diferentes es una forma de que una llamada a función tenga una semántica diferente de un valor y, por lo tanto, rompa la transparencia referencial.

Digamos que escribimos el código f1("hello", "world"), lo ejecutamos y obtenemos el valor de retorno "hello". Si hacemos una búsqueda / reemplazo de cada llamada f1("hello", "world")y la reemplazamos con "hello"habremos cambiado la semántica del programa (todas las llamadas ahora serán reemplazadas por "hello", pero originalmente la mitad de ellas se habrían evaluado "world"). Por lo tanto, las llamadas a f1no son referencialmente transparentes, por f1lo tanto, son impuras.

Otra forma en que una llamada de función puede tener una semántica diferente a un valor es mediante la ejecución de sentencias. Por ejemplo:

function f2(x) {
  console.log("foo");
  return x;
}

El valor de retorno de f2("bar")siempre será "bar", pero la semántica del valor "bar"es diferente de la llamada f2("bar")ya que este último también se registrará en la consola. Reemplazar uno con el otro cambiaría la semántica del programa, por lo que no es referencialmente transparente y, por f2lo tanto, es impuro.

Si su dollarToEurofunción es referencialmente transparente (y por lo tanto pura) depende de dos cosas:

  • El 'alcance' de lo que consideramos referencialmente transparente
  • Si exchangeRatealguna vez cambiará dentro de ese 'alcance'

No hay un "mejor" alcance para usar; normalmente pensaríamos en una sola ejecución del programa, o la vida útil del proyecto. Como analogía, imagine que los valores de retorno de cada función se almacenan en caché (como la tabla de notas en el ejemplo dado por @ aadit-m-shah): cuándo tendríamos que borrar el caché, para garantizar que los valores obsoletos no interfieran con nuestro ¿semántica?

Si lo exchangeRateestuviera utilizando var, podría cambiar entre cada llamada a dollarToEuro; necesitaríamos borrar los resultados almacenados en caché entre cada llamada, por lo que no habría transparencia referencial de la que hablar.

Al usarlo const, estamos ampliando el 'alcance' a una ejecución del programa: sería seguro almacenar en caché los valores de retorno dollarToEurohasta que el programa finalice. Podríamos imaginar el uso de una macro (en un lenguaje como Lisp) para reemplazar las llamadas a funciones con sus valores de retorno. Esta cantidad de pureza es común para cosas como valores de configuración, opciones de línea de comandos o ID únicos. Si nos limitamos a pensar en una ejecución del programa, obtenemos la mayoría de los beneficios de la pureza, pero tenemos que tener cuidado con las ejecuciones (por ejemplo, guardar datos en un archivo y luego cargarlos en otra ejecución). No llamaría a tales funciones "puras" en sentido abstracto (por ejemplo, si estuviera escribiendo una definición de diccionario), pero no tengo ningún problema en tratarlas como puras en su contexto .

Si tratamos la vida útil del proyecto como nuestro 'alcance', entonces somos los "más transparentes de referencia" y, por lo tanto, los "más puros", incluso en sentido abstracto. Nunca necesitaríamos limpiar nuestro caché hipotético. Incluso podríamos hacer este "almacenamiento en caché" reescribiendo directamente el código fuente en el disco, para reemplazar las llamadas con sus valores de retorno. Esto incluso funcionaría en todos los proyectos, por ejemplo, podríamos imaginar una base de datos en línea de funciones y sus valores de retorno, donde cualquiera puede buscar una llamada a la función y (si está en la base de datos) usar el valor de retorno proporcionado por alguien al otro lado del mundo que usó una función idéntica hace años en un proyecto diferente.

Warbo
fuente
4

Como está escrito, es una función pura. No produce efectos secundarios. La función tiene un parámetro formal, pero tiene dos entradas y siempre generará el mismo valor para cualquiera de las dos entradas.

11112222233333
fuente
2

¿Podemos llamar a tales funciones funciones puras? Si la respuesta es NO, ¿cómo podemos refactorizarlo para que sea uno?

Como notó debidamente, "podría darme una salida diferente mañana" . Si ese fuera el caso, la respuesta sería un rotundo "no" . Esto es especialmente así si su comportamiento previsto de dollarToEuroha sido interpretado correctamente como:

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

Sin embargo, existe una interpretación diferente, donde se consideraría pura:

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro directamente arriba es puro.


Desde una perspectiva de ingeniería de software, es esencial declarar la dependencia de dollarToEurola función fetchFromDatabase. Por lo tanto, refactorice la definición de la dollarToEurosiguiente manera:

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

Con este resultado, dada la premisa que fetchFromDatabasefunciona satisfactoriamente, podemos concluir que la proyección de fetchFromDatabaseon dollarToEurodebe ser satisfactoria. O la afirmación " fetchFromDatabasees puro" implica que dollarToEuroes puro (ya que fetchFromDatabasees una base para dollarToEuroel factor escalar de x.

De la publicación original, puedo entender que fetchFromDatabasees un tiempo de función. Mejoremos el esfuerzo de refactorización para que esa comprensión sea transparente, por lo tanto, calificamos claramente fetchFromDatabasecomo una función pura:

fetchFromDatabase = (marca de tiempo) => {/ * aquí va la implementación * /};

Finalmente, refactorizaría la función de la siguiente manera:

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

En consecuencia, dollarToEuropuede probarse unitariamente simplemente demostrando que llama correctamente fetchFromDatabase(o su derivada exchangeRate).

Igwe Kalu
fuente
1
Esto fue muy esclarecedor. +1. Gracias.
Muñeco de nieve el
Si bien encuentro su respuesta más informativa, y quizás la mejor refactorización para el caso de uso particular de dollarToEuro; He mencionado en el OP que podría haber otros casos de uso. Elegí dollarToEuro porque instantáneamente evoca lo que estoy tratando de hacer, pero podría haber algo menos sutil que depende de una variable libre que puede cambiar, pero no necesariamente en función del tiempo. Con eso en mente, considero que el refactor más votado es el más accesible y el que puede ayudar a otros con casos de uso similares. Gracias por tu ayuda a pesar de todo.
Muñeco de nieve el
-1

Soy un bilingüe de Haskell / JS y Haskell es uno de los idiomas que hace mucho por la pureza de la función, por lo que pensé que le daría la perspectiva desde la perspectiva de Haskell.

Como otros han dicho, en Haskell, leer una variable mutable generalmente se considera impuro. Hay una diferencia entre variables y definiciones en que las variables pueden cambiar más tarde, las definiciones son las mismas para siempre. Entonces, si lo hubiera declarado const(suponiendo que sea solo una numbery no tenga una estructura interna mutable), leer de eso sería usar una definición, que es pura. Pero quería modelar los tipos de cambio que cambian con el tiempo, y eso requiere algún tipo de mutabilidad y luego entra en impureza.

Para describir ese tipo de cosas impuras (podemos llamarlas "efectos" y su uso "efectivo" en lugar de "puro") en Haskell, hacemos lo que podríamos llamar metaprogramación . Hoy la metaprogramación generalmente se refiere a macros, que no es lo que quiero decir, sino más bien la idea de escribir un programa para escribir otro programa en general.

En este caso, en Haskell, escribimos un cómputo puro que computa un programa efectivo que luego hará lo que queramos. Entonces, el objetivo de un archivo fuente de Haskell (al menos, uno que describe un programa, no una biblioteca) es describir un cálculo puro para un programa efectivo que produce vacío, llamado main. Entonces, el trabajo del compilador de Haskell es tomar este archivo fuente, realizar ese cálculo puro y poner ese programa efectivo como un ejecutable binario en algún lugar de su disco duro para ejecutarlo más tarde cuando lo desee. Hay una brecha, en otras palabras, entre el momento en que se ejecuta la computación pura (mientras el compilador hace el ejecutable) y el momento en que se ejecuta el programa efectivo (cada vez que ejecuta el ejecutable).

Entonces, para nosotros, los programas efectivos son realmente una estructura de datos y no hacen intrínsecamente nada solo por el hecho de ser mencionados (no tienen efectos secundarios * además de su valor de retorno; su valor de retorno contiene sus efectos). Para un ejemplo muy ligero de una clase TypeScript que describe programas inmutables y algunas cosas que puede hacer con ellos,

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

La clave es que si tiene un problema, Program<x>entonces no se han producido efectos secundarios y estas son entidades totalmente funcionalmente puras. El mapeo de una función sobre un programa no tiene ningún efecto secundario a menos que la función no sea pura; secuenciar dos programas no tiene ningún efecto secundario; etc.

Entonces, por ejemplo, cómo aplicar esto en su caso, puede escribir algunas funciones puras que devuelven programas para obtener usuarios por ID y alterar una base de datos y obtener datos JSON, como

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

y luego podría describir un trabajo cron para curvar una URL y buscar algún empleado y notificar a su supervisor de una manera puramente funcional como

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

El punto es que cada función aquí es una función completamente pura; nada ha sucedido hasta que realmente lo puse action.run()en movimiento. Además puedo escribir funciones como,

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

y si JS prometiera la cancelación, podríamos hacer que dos programas compitan entre sí y tomar el primer resultado y cancelar el segundo. (Quiero decir que aún podemos, pero queda menos claro qué hacer).

De manera similar, en su caso, podemos describir los tipos de cambio cambiantes

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

y exchangeRatepodría ser un programa que mira un valor mutable,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

pero aun así, esta función dollarsToEuros ahora es una función pura de un número a un programa que produce un número, y puede razonar sobre eso de esa manera equitativa determinista que puede razonar sobre cualquier programa que no tenga efectos secundarios.

El costo, por supuesto, es que eventualmente tienes que llamar a eso en .run() algún lugar , y eso será impuro. Pero toda la estructura de su cómputo puede describirse mediante un cómputo puro, y puede llevar la impureza a los márgenes de su código.

CR Drost
fuente
Tengo curiosidad de por qué esto sigue siendo votado negativamente, pero quiero decir que todavía lo mantengo (de hecho, es cómo manipulas los programas en Haskell donde las cosas son puras por defecto) y con mucho gusto los votos negativos. Aún así, si los votantes negativos quisieran dejar comentarios explicando lo que no les gusta, puedo tratar de mejorarlo.
CR Drost
Sí, me preguntaba por qué hay tantos votos negativos, pero ni un solo comentario, además del autor.
Buda Örs