javascript function functional-programming

javascript - ¿Es esta una función pura?



function functional-programming (10)

La mayoría de las sources 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 - Usando const en la primera línea. Usé let antes sin querer).

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 una base de datos 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?


¿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 cierto si su comportamiento previsto de dollarToEuro se ha 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 dollarToEuro en la función fetchFromDatabase . Por lo tanto, refactorice la definición de dollarToEuro siguiente manera:

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

Con este resultado, dada la premisa de que fetchFromDatabase funciona satisfactoriamente, podemos concluir que la proyección de fetchFromDatabase en dollarToEuro debe ser satisfactoria. O la afirmación " fetchFromDatabase es puro" implica dollarToEuro es puro (dado que fetchFromDatabase es una base para dollarToEuro por el factor escalar de x .

De la publicación original, puedo entender que fetchFromDatabase es un tiempo de función. Mejoremos el esfuerzo de refactorización para que esa comprensión sea transparente, por lo tanto, califique claramente fetchFromDatabase como 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, dollarToEuro puede probarse por unidad simplemente demostrando que llama correctamente fetchFromDatabase (o su derivada exchangeRate ).


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.


Como han dicho otras respuestas, la forma en que 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, dollarToEuro parece 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 dollarToEuro sino 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 dollarToEuro general los 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 completamente pura y autosuficiente del dollarToEuro general, 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 instante 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 dollarToEuro que siempre obtiene el valor más actualizado toma esencialmente el parámetro de fecha implícitamente, configurándolo en el instante en que se llama a la función, que no es pura simplemente porque nunca puede llamar a la función con los mismos parámetros dos veces .

Si desea tener una versión pura de dollarToEuro que aún pueda obtener el valor más actualizado, 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, finalizando con algo como esto:

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


Como otros han dicho, leer una variable mutable generalmente se considera impuro. Si lo hubiera declarado const (suponiendo que es solo un number y no tiene una estructura interna mutable) eso sería puro.

Para refactorizar dicho código para que sea puro en lenguajes de programación funcionales como Haskell, hacemos metaprogramación , es decir, escribimos código que manipula programas inmutables; esos programas en sí mismos no hacen nada, sino que simplemente existen.

Entonces, para un ejemplo muy ligero de una clase que describe programas inmutables y algunas cosas que puedes 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 Program<x> entonces no se han producido efectos secundarios y estas son entidades totalmente funcionales. 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, escribiríamos algunos programas para obtener usuarios por ID y para 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íamos 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.

Del mismo modo podríamos tener

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

y exchangeRate podrí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 ello de esa manera equitativa determinista que puede razonar sobre cualquier programa que no tenga efectos secundarios.


El valor de retorno de dollarToEuro depende de una variable externa que no es un argumento, por lo que es impuro.

En la respuesta es NO, ¿cómo podemos refactorizarlo para que sea uno?

Una opción sería pasar en exchangeRate . De esa manera, cada vez que los argumentos son (something, somethingElse) , se garantiza que la salida sea exactamente 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, definitivamente debe evitar let también: siempre use const , para evitar la reasignación.


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 exchangeRate como 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())


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 exchangeRate nunca puede modificarse entre las dos llamadas a dollarToEuro(100) , es posible memorizar el resultado de la primera llamada a dollarToEuro(100) y optimizar la segunda llamada. El resultado será el mismo, por lo que podemos recordar el valor de antes.

El exchangeRate puede establecerse una vez, antes de llamar a cualquier función que lo busque, y nunca modificarse. De manera menos restrictiva, es posible que tenga un código que busque el exchangeRate una vez para 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ía derecho a asumir que, si no actualizó el tipo de cambio, nadie más lo ha cambiado en usted.

Si fetchFromDatabase() es en sí mismo una función pura que evalúa a una constante, y exchangeRate es inmutable, podríamos doblar esta constante durante todo el cálculo. Un compilador que sabe que este es el caso podría hacer la misma deducción que hizo en el comentario, que dollarToEuro(100) evalúa a 90.0, y reemplaza la expresión completa 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.


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 f1 es 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 las reemplazamos con "hello" habremos cambiado la semántica del programa (todas las llamadas ahora serán reemplazadas por "hello" , pero originalmente aproximadamente la mitad de ellos habría evaluado a "world" ). Por lo tanto, las llamadas a f1 no son referencialmente transparentes, por lo tanto, f1 es impuro.

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 por otro cambiaría la semántica del programa, por lo que no es referencialmente transparente y, por lo tanto, f2 es impuro.

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

  • El ''alcance'' de lo que consideramos referencialmente transparente
  • Si la tasa de exchangeRate cambiará alguna vez 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 exchangeRate usara var entonces 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 usar const estamos ampliando el ''alcance'' a una ejecución del programa: sería seguro almacenar en caché los valores de retorno de dollarToEuro hasta 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 debemos tener cuidado en 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 de 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.


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");

¿La función de greet es 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 de greet y no cambió la semántica del programa.

Ahora, considere el siguiente programa.

undefined; undefined;

Aquí, reemplazamos las aplicaciones de la función greet 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 función de greet 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 función main es impura. Sin embargo, ¿la función timeDiff es pura o impura? Aunque depende de serverTime que proviene de una llamada de red impura, todavía es 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 answer , dijo que la función dollarToEuro 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 el exchangeRate provenga de una base de datos es irrelevante. Es un detalle interno y nuestra metodología de caja negra 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 llama unsafePerformIO , 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 usamos unsafePerformIO , 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 exchangeRate no se define como const , se modificará mientras se ejecuta el programa. Si ese es el caso, dollarToEuro es definitivamente una función impura porque cuando se modifica la dollarToEuro exchangeRate , se romperá la transparencia referencial.

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

Tenga en cuenta que el valor de exchangeRate puede 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 ejemplo timeDiff varias veces, obtendrá diferentes valores para serverTime y, por lo tanto, diferentes resultados. Sin embargo, debido a que el valor de serverTime nunca cambia mientras se ejecuta el programa, la función timeDiff es pura.


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

En un lenguaje tan dinámico como JS, con tantas posibilidades de Object.prototype.valueOf tipos base de parches, o inventar tipos personalizados usando características como Object.prototype.valueOf es imposible saber si una función es pura con solo mirarla, ya que depende de pregunta si quieren 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 sources

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://.com/a/58749249/251311

Otros recursos relevantes: