La mayoría de las fuentes definen una función pura que tiene las siguientes dos propiedades:
- Su valor de retorno es el mismo para los mismos argumentos.
- 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 const
en la primera línea. Utilizado let
anteriormente 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?
fuente
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
(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 porqueexchangeRate
no puede cambiar.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 tardeRespuestas:
El
dollarToEuro
valor de retorno de 's depende de una variable externa que no es un argumento; por lo tanto, la función es impura.Una opción es pasar
exchangeRate
. De esta manera, cada vez que se presentan argumentos(something, somethingElse)
, se garantiza que la salida seasomething * somethingElse
:Tenga en cuenta que para la programación funcional, debe evitar
let
, siempre useconst
para evitar la reasignación.fuente
const add = x => y => x + y; const one = add(42);
Aquí tantoadd
yone
son funciones puras.const foo = 42; const add42 = x => x + foo;
<- esta es otra función pura, que nuevamente utiliza variables libres.dollarToEuro
función del ejemplo en su respuesta es impura porque depende de la variable libreexchangeRate
. 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 ladollarToEuro
función es impura porque depende de laexchangeRate
que proviene de una base de datos. Él dice que es impuro porque "depende de la IO transitivamente".dollarToEuro
es impuro porqueexchangeRate
es una variable libre. Sugiere que siexchangeRate
no fuera una variable libre, es decir, si fuera un argumento, entoncesdollarToEuro
sería puro. Por lo tanto, sugiere quedollarToEuro(100)
es impuro perodollarToEuro(100, exchangeRate)
es puro. Eso es claramente absurdo porque en ambos casos dependes delexchangeRate
que proviene de una base de datos. La única diferencia es si es o noexchangeRate
una variable libre dentro de ladollarToEuro
función.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 deeax
", 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.
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.
¿Es la
greet
función pura o impura? Según nuestra metodología de recuadro negro, si le damos la misma entrada (por ejemploWorld
), siempre imprime la misma salida en la pantalla (es decirHello 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.
Aquí, subrayamos la definición
greet
y no cambió la semántica del programa.Ahora, considere el siguiente programa.
Aquí, reemplazamos las aplicaciones de la
greet
funció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 lagreet
función es impura. No es referencialmente transparente.Ahora, consideremos otro ejemplo. Considere el siguiente programa.
Claramente, la
main
función es impura. Sin embargo, es eltimeDiff
función pura o impura? Aunque depende deserverTime
cuá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 el
dollarToEuro
función en el siguiente ejemplo es impura porque "depende de la IO transitivamente".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 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.
Aquí, supongo que debido a que
exchangeRate
no está definido comoconst
, se modificará mientras se ejecuta el programa. Si ese es el caso, entoncesdollarToEuro
definitivamente es una función impura porque cuandoexchangeRate
se modifica, se romperá la transparencia referencial.Sin embargo, si la
exchangeRate
variable no se modifica y nunca se modificará en el futuro (es decir, si es un valor constante), aunque se defina comolet
, no se 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
timeDiff
ejemplo varias veces, obtendrá diferentes valoresserverTime
y, por lo tanto, diferentes resultados. Sin embargo, debido a que el valor deserverTime
nunca cambia mientras se ejecuta el programa, latimeDiff
función es pura.fuente
const
en mi ejemplo.const
entonces ladollarToEuro
función es pura. La única forma en que el valor deexchangeRate
cambiarí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.eax
se 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.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:
Una respuesta de mí-pragmatista:
Desde la definición misma de wikipedia
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:
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:
fuente
me
como zerkms que proporciona una respuesta.add42
y miaddX
es puramente que mix
puede ser cambiado y tuft
no puede ser cambiado (y por lo tanto,add42
el valor de retorno no varía segúnft
).dollarToEuro
función en su ejemplo sea impura. Le expliqué por qué no estoy de acuerdo con mi respuesta. stackoverflow.com/a/58749249/783743Como han dicho otras respuestas, la forma en que lo ha implementado
dollarToEuro
,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 implementadodollarToEuro
perodollarToEuroAtInstantOfProgramStart
.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
dollarToEuro
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 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í: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
dollarToEuro
eso 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
dollarToEuro
eso, 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:fuente
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:
Si
exchangeRate
nunca se pudo modificar entre las dos llamadas adollarToEuro(100)
, es posible memorizar el resultado de la primera llamadadollarToEuro(100)
y optimizar la segunda llamada. El resultado será el mismo, por lo que podemos recordar el valor de antes.Se
exchangeRate
puede 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 unaexchangeRate
vez 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 yexchangeRate
es 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 sedollarToEuro(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.fuente
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.
Código de ejemplo:
fuente
const
.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):¿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 llamadaf1("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 af1
no son referencialmente transparentes, porf1
lo 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:
El valor de retorno de
f2("bar")
siempre será"bar"
, pero la semántica del valor"bar"
es diferente de la llamadaf2("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, porf2
lo tanto, es impuro.Si su
dollarToEuro
función es referencialmente transparente (y por lo tanto pura) depende de dos cosas:exchangeRate
alguna 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
exchangeRate
estuviera utilizandovar
, podría cambiar entre cada llamada adollarToEuro
; 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 retornodollarToEuro
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 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.
fuente
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.
fuente
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
dollarToEuro
ha sido interpretado correctamente como:Sin embargo, existe una interpretación diferente, donde se consideraría pura:
dollarToEuro
directamente arriba es puro.Desde una perspectiva de ingeniería de software, es esencial declarar la dependencia de
dollarToEuro
la funciónfetchFromDatabase
. Por lo tanto, refactorice la definición de ladollarToEuro
siguiente manera:Con este resultado, dada la premisa que
fetchFromDatabase
funciona satisfactoriamente, podemos concluir que la proyección defetchFromDatabase
ondollarToEuro
debe ser satisfactoria. O la afirmación "fetchFromDatabase
es puro" implica quedollarToEuro
es puro (ya quefetchFromDatabase
es una base paradollarToEuro
el factor escalar dex
.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, calificamos claramentefetchFromDatabase
como una función pura:fetchFromDatabase = (marca de tiempo) => {/ * aquí va la implementación * /};
Finalmente, refactorizaría la función de la siguiente manera:
En consecuencia,
dollarToEuro
puede probarse unitariamente simplemente demostrando que llama correctamentefetchFromDatabase
(o su derivadaexchangeRate
).fuente
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.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 unanumber
y 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,
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
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
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,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
y
exchangeRate
podría ser un programa que mira un valor mutable,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.fuente