¿Se consideran impuros los cierres en la programación funcional?
Parece que generalmente se pueden evitar cierres pasando valores directamente a una función. Por lo tanto, ¿deben evitarse los cierres cuando sea posible?
Si son impuros, y estoy en lo cierto al afirmar que se pueden evitar, ¿por qué tantos lenguajes de programación funcionales admiten cierres?
Uno de los criterios para una función pura es que "la función siempre evalúa el mismo valor de resultado dados los mismos valores de argumento ".
Suponer
f: x -> x + y
f(3)
No siempre dará el mismo resultado. f(3)
depende del valor del y
cual no es un argumento de f
. Por f
lo tanto, no es una función pura.
Dado que todos los cierres se basan en valores que no son argumentos, ¿cómo es posible que un cierre sea puro? Sí, en teoría, el valor cerrado podría ser constante, pero no hay forma de saberlo con solo mirar el código fuente de la función en sí.
A donde esto me lleva es que la misma función puede ser pura en una situación pero impura en otra. Uno no siempre puede determinar si una función es pura o no mediante el estudio de su código fuente. Por el contrario, es posible que tenga que considerarlo en el contexto de su entorno en el momento en que se llama antes de que se pueda hacer tal distinción.
¿Estoy pensando en esto correctamente?
fuente
y
no puede cambiar, por lo que la salida def(3)
siempre será la misma.y
es parte de la definición def
aunque no esté marcado explícitamente como una entrada paraf
- sigue siendo el caso quef
se define en términos dey
(podríamos denotar la función f_y, para hacer que la dependencia seay
explícita), y por lo tanto el cambioy
da una función diferente . La función particularf_y
definida para un particulary
es muy pura. (Por ejemplo, las dos funcionesf: x -> x + 3
yf: x -> x + 5
son diferentes funciones, y ambos puro, a pesar de que pasó a utilizar la misma letra para denotar ellos.)Respuestas:
La pureza se puede medir por dos cosas:
Si la respuesta a 1 es sí y la respuesta a 2 es no, entonces la función es pura. Los cierres solo impiden una función si modifica la variable cerrada.
fuente
y
. Entonces, por ejemplo, definimos una funcióng
tal queg(y)
es en sí misma la funciónx -> x + y
. Entoncesg
es una función de enteros que devuelve funciones,g(3)
es una función de enteros que devuelve enteros yg(2)
es una función diferente de enteros que devuelve enteros. Las tres funciones son puras.Aparecen los cierres de Lambda Calculus, que es la forma más pura de programación funcional posible, por lo que no los llamaría "impuros" ...
Los cierres no son "impuros" porque las funciones en lenguajes funcionales son ciudadanos de primera clase, lo que significa que pueden tratarse como valores.
Imagina esto (pseudocódigo):
y
es un valor Su valor depende dex
, perox
es inmutable, por lo quey
el valor también es inmutable. Podemos llamarfoo
muchas veces con diferentes argumentos que producirán diferentesy
s, peroy
todos ellos viven en diferentes ámbitos y dependen de diferentesx
s para que la pureza permanezca intacta.Ahora vamos a cambiarlo:
Aquí estamos usando un cierre (estamos cerrando sobre x), pero es igual que en
foo
- diferentes llamadas abar
con diferentes argumentos crean diferentes valores dey
(recuerde, las funciones son valores) que son todos inmutables para que la pureza permanezca intacta.Además, tenga en cuenta que los cierres tienen un efecto muy similar al curry:
baz
no es realmente diferente debar
: en ambos creamos un valor de función llamadoy
que devuelve su argumento plusx
. De hecho, en Lambda Calculus usas cierres para crear funciones con múltiples argumentos, y todavía no es impuro.fuente
Otros han cubierto muy bien la pregunta general en sus respuestas, por lo que solo trataré de aclarar la confusión que señalas en tu edición.
El cierre no se convierte en una entrada de la función, sino que "va" al cuerpo de la función. Para ser más concreto, una función se refiere a un valor en el alcance externo en su cuerpo.
Tiene la impresión de que hace que la función sea impura. Ese no es el caso, en general. En la programación funcional, los valores son inmutables la mayor parte del tiempo . Eso se aplica también al valor cerrado.
Digamos que tienes un código como este:
Llamando
make 3
ymake 4
le dará dos funciones con cierres sobremake
ely
argumento. Uno de ellos regresaráx + 3
, el otrox + 4
. Sin embargo, son dos funciones distintas, y ambas son puras. Fueron creados usando la mismamake
función, pero eso es todo.Tenga en cuenta la mayor parte del tiempo unos pocos párrafos atrás.
Tenga en cuenta que para 2 y 3, esos idiomas no ofrecen ninguna garantía sobre la pureza. La impureza allí no es una propiedad del cierre, sino del lenguaje en sí. Los cierres no cambian mucho la imagen por sí mismos.
fuente
IO A
valor inmutable , y su tipo de cierre esIO (B -> C)
o somesuch. Se mantiene la purezaNormalmente le pediría que aclare su definición de "impuro", pero en este caso realmente no importa. Suponiendo que está contrastando el término puramente funcional , la respuesta es "no", porque no hay nada en los cierres que sea inherentemente destructivo. Si su lenguaje fuera puramente funcional sin cierres, seguiría siendo puramente funcional con cierres. Si, en cambio, quiere decir "no funcional", la respuesta sigue siendo "no"; los cierres facilitan la creación de funciones.
Sí, pero su función tendría un parámetro más, y eso cambiaría su tipo. Los cierres le permiten crear funciones basadas en variables sin agregar parámetros. Esto es útil cuando tiene, por ejemplo, una función que toma 2 argumentos, y desea crear una versión que solo tome 1 argumento.
EDITAR: con respecto a su propia edición / ejemplo ...
Depende es la elección incorrecta de la palabra aquí. Citando el mismo artículo de Wikipedia que hiciste:
Suponiendo que
y
es inmutable (que suele ser el caso en lenguajes funcionales), se cumple la condición 1: para todos los valores dex
, el valor def(x)
no cambia. Esto debería quedar claro por el hecho de quey
no es diferente de una constante, yx + 3
es puro. También está claro que no hay mutación o E / S.fuente
Muy rápidamente: una sustitución es "referencialmente transparente" si "sustituir lo similar conduce a lo similar" y una función es "pura" si todos sus efectos están contenidos en su valor de retorno. Ambos pueden hacerse precisos, pero es vital tener en cuenta que no son idénticos ni siquiera uno implica al otro.
Ahora hablemos de los cierres.
"Cierres" aburridos (en su mayoría puros)
Los cierres ocurren porque cuando evaluamos un término lambda interpretamos las variables (enlazadas) como búsquedas de entorno. Por lo tanto, cuando devolvemos un término lambda como resultado de una evaluación, las variables dentro de él habrán "cerrado" los valores que tomaron cuando se definió.
En el simple cálculo lambda esto es algo trivial y toda la noción simplemente desaparece. Para demostrar eso, aquí hay un intérprete de cálculo lambda relativamente liviano:
La parte importante a tener en cuenta es
addEnv
cuando aumentamos el entorno con un nuevo nombre. Esta función se llama solo "dentro" delAbs
término de tracción interpretado (término lambda). El entorno se "busca" cada vez que evaluamos unVar
término y, por lo tanto, seVar
resuelve lo que sea que seName
mencionó en elEnv
que fue capturado por laAbs
tracción que contiene elVar
.Ahora, nuevamente, en términos simples de LC esto es aburrido. Significa que las variables enlazadas son solo constantes en lo que a nadie le importa. Se evalúan de forma directa e inmediata como los valores que denotan en el entorno como alcances léxicos hasta ese punto.
Esto también es (casi) puro. El único significado de cualquier término en nuestro cálculo lambda está determinado por su valor de retorno. La única excepción es el efecto secundario de la no terminación que se incorpora en el término Omega:
Cierres (impuros) interesantes
Ahora, para ciertos entornos, los cierres descritos en el LC simple anterior son aburridos porque no existe la noción de poder interactuar con las variables que hemos cerrado. En particular, la palabra "cierre" tiende a invocar código como el siguiente Javascript
Esto demuestra que hemos cerrado la
n
variable en la función internaincr
y que la llamadaincr
interactúa significativamente con esa variable.mk_counter
es puro, peroincr
es decididamente impuro (y tampoco es referencialmente transparente).¿Qué difiere entre estas dos instancias?
Nociones de "variable"
Si observamos qué significan sustitución y abstracción en el sentido claro de LC, notamos que son decididamente simples. Las variables son literalmente nada más que búsquedas de entorno inmediatas. Lambda abstracción es, literalmente, nada más que la creación de un entorno aumentada para evaluar la expresión interior. No hay espacio en este modelo para el tipo de comportamiento que vimos con
mk_counter
/incr
porque no hay variación permitida.Para muchos, este es el corazón de lo que significa "variable": variación. Sin embargo, a los semánticos les gusta distinguir entre el tipo de variable utilizada en LC y el tipo de "variable" utilizada en Javascript. Para hacerlo, tienden a llamar a este último una "celda mutable" o "ranura".
Esta nomenclatura sigue el largo uso histórico de "variable" en matemáticas donde significaba algo más como "desconocido": la expresión (matemática) "x + x" no permite
x
variar con el tiempo, sino que tiene un significado independientemente del valor (único, constante)x
toma.Por lo tanto, decimos "ranura" para enfatizar la capacidad de poner valores en una ranura y eliminarlos.
Para agregar más a la confusión, en Javascript estas "ranuras" se ven igual que las variables: escribimos
para crear uno y luego cuando escribimos
nos indica buscando el valor actualmente almacenado en ese espacio. Para hacer esto más claro, los lenguajes puros tienden a pensar en las tragamonedas como nombres de nombres (matemáticos, cálculo lambda). En este caso, debemos etiquetar explícitamente cuándo obtenemos o colocamos desde una ranura. Tal notación tiende a parecerse a
La ventaja de esta notación es que ahora tenemos una distinción firme entre variables matemáticas y ranuras mutables. Las variables pueden tomar ranuras como sus valores, pero la ranura particular nombrada por una variable es constante en todo su alcance.
Usando esta notación podemos reescribir el
mk_counter
ejemplo (esta vez en una sintaxis similar a la de Haskell, aunque decididamente una semántica similar a la de Haskell):En este caso, estamos utilizando procedimientos que manipulan esta ranura mutable. Para implementarlo, necesitaríamos cerrar no solo un entorno constante de nombres,
x
sino también un entorno mutable que contenga todos los espacios necesarios. Esto está más cerca de la noción común de "cierre" que la gente ama tanto.De nuevo,
mkCounter
es muy impuro. También es muy referencialmente opaco. Pero tenga en cuenta que los efectos secundarios no surgen de la captura o el cierre del nombre, sino de la captura de la célula mutable y las operaciones de efectos secundarios en ella comoget
yput
.En última instancia, creo que esta es la respuesta final a su pregunta: la pureza no se ve afectada por la captura de variables (matemáticas) sino por las operaciones de efecto secundario realizadas en ranuras mutables nombradas por variables capturadas.
Es solo que en los idiomas que no intentan estar cerca de LC o no intentan mantener la pureza, estos dos conceptos a menudo se combinan y generan confusión.
fuente
No, los cierres no hacen que una función sea impura, siempre que el valor cerrado sea constante (ni modificado por el cierre ni por otro código), que es el caso habitual en la programación funcional.
Tenga en cuenta que si bien siempre puede pasar un valor como argumento, generalmente no puede hacerlo sin una cantidad considerable de dificultad. Por ejemplo (coffeescript):
Por su sugerencia, puede regresar:
Esta función no se llama en este momento, solo se define , por lo que tendría que encontrar alguna forma de pasar el valor deseado
closedValue
al punto en el que realmente se llama a la función. En el mejor de los casos, esto crea mucho acoplamiento. En el peor de los casos, no controlas el código en el punto de llamada, por lo que es efectivamente imposible.Las bibliotecas de eventos en idiomas que no admiten cierres suelen proporcionar alguna otra forma de devolver datos arbitrarios a la devolución de llamada, pero no son bonitas y crean una gran complejidad tanto para el responsable de la biblioteca como para los usuarios de la biblioteca.
fuente