¿Se considera pura una función pura memorizada?

47

Digamos que fn(x)es una función pura que hace algo costoso, como devolver una lista de los factores primos de x.

Y digamos que hacemos una versión memorable de la misma función llamada memoizedFn(x). Siempre devuelve el mismo resultado para una entrada dada, pero mantiene un caché privado de resultados anteriores para mejorar el rendimiento.

Hablando formalmente, ¿se memoizedFn(x)considera puro?

¿O hay algún otro nombre o término de calificación utilizado para referirse a dicha función en las discusiones de FP? (es decir, una función con efectos secundarios que pueden afectar la complejidad computacional de las llamadas posteriores pero que pueden no afectar los valores de retorno).

callum
fuente
24
Tal vez no sea puro para los puristas, sino "lo suficientemente puro" para las personas pragmáticas ;-)
Doc Brown
2
@DocBrown Estoy de acuerdo, solo me pregunto si hay un término más formal para 'lo suficientemente puro'
callum
13
La ejecución de una función pura probablemente modificará la memoria caché de instrucciones del procesador, los predictores de ramificación, etc.
gnasher729
10
@callum No, no existe una definición formal de "lo suficientemente puro". Al discutir sobre la pureza y la equivalencia semántica de dos llamadas "referencialmente transparentes", siempre debe indicar exactamente qué semántica va a aplicar. Con un bajo nivel de detalle de implementación, siempre se descompondrá y tendrá diferentes efectos de memoria o tiempos. Es por eso que debes ser pragmático: ¿qué nivel de detalle es útil para razonar sobre tu código?
Bergi
3
Entonces, en aras del pragmatismo, diría que la pureza depende de si consideras o no el tiempo de cálculo como parte de la salida. funcx(){sleep(cached_time--); return 0;}devuelve el mismo valor cada vez, pero tendrá un rendimiento diferente
Marte,

Respuestas:

41

Si. La versión memorable de una función pura también es una función pura.

Lo único que le importa a la pureza de la función es el efecto que tienen los parámetros de entrada en el valor de retorno de la función (pasar la misma entrada siempre debe producir la misma salida) y cualquier efecto secundario relevante para los estados globales (p. Ej., Texto al terminal o UI o red) . El tiempo de cómputo y el uso de memoria adicional es irrelevante para la pureza de la función.

Las memorias caché de una función pura son prácticamente invisibles para el programa; un lenguaje de programación funcional puede optimizar automáticamente una función pura a una versión memorable de la función si puede determinar que será beneficioso hacerlo. En la práctica, determinar automáticamente cuándo la memorización es beneficiosa es en realidad un problema bastante difícil, pero dicha optimización sería válida.

Lie Ryan
fuente
19

Wikipedia define una "Función Pura" como una función que tiene las siguientes propiedades:

  • 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).

  • 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 efecto, una función pura devuelve la misma salida dada la misma entrada, y no afecta nada más fuera de la función. Para fines de pureza, no importa cómo la función calcule su valor de retorno, siempre que devuelva la misma salida dada la misma entrada.

Los lenguajes funcionalmente puros como Haskell usan rutinariamente la memorización para acelerar una función almacenando en caché sus resultados previamente calculados.

Robert Harvey
fuente
16
Puedo extrañar algo, pero ¿cómo vas a mantener el caché sin efectos secundarios?
val
1
Manteniéndolo dentro de la función.
Robert Harvey
44
"sin mutación de la variable estática local" parece excluir también las variables locales persistentes entre llamadas.
val
3
En realidad, esto no responde a la pregunta, incluso si parece implicar que sí, es puro.
Marte
66
@val Tienes razón: esta condición necesita ser relajada un poco. La memoria puramente funcional a la que se refiere no tiene una mutación visible de ningún dato estático. Lo que sucede es que el resultado se calcula y se memoriza la primera vez que se llama a la función, y devuelve el mismo valor cada vez que se llama. Muchos lenguajes tienen un modismo para eso: una static constvariable local en C ++ (pero no C), o una estructura de datos evaluada de manera perezosa en Haskell. Hay una condición más que necesita: la inicialización debe ser segura para subprocesos.
Davislor
7

Sí, las funciones puras memorizadas se conocen comúnmente como puras. Esto es especialmente común en lenguajes como Haskell, en los que los resultados inmutables, evaluados de forma perezosa y memorizados son una característica incorporada.

Hay una advertencia importante: la función de memorización debe ser segura para subprocesos, o de lo contrario podría obtener una condición de carrera cuando dos subprocesos intentan llamarla.

Un ejemplo de un científico informático que usa el término "puramente funcional" de esta manera es esta publicación de blog de Conal Elliott sobre la memorización automática:

Quizás sorprendentemente, la memorización puede implementarse de manera simple y puramente funcional en un lenguaje funcional vago.

Hay muchos ejemplos en la literatura revisada por pares, y lo han sido durante décadas. Por ejemplo, este documento de 1995, "Uso de la memorización automática como herramienta de ingeniería de software en sistemas de IA del mundo real", utiliza un lenguaje muy similar en la sección 5.2 para describir lo que hoy llamaríamos una función pura:

La memorización solo funciona para funciones reales, no para procedimientos. Es decir, si el resultado de una función no se especifica completa y determinísticamente por sus parámetros de entrada, el uso de la memorización dará resultados incorrectos. El número de funciones que se pueden memorizar con éxito aumentará al fomentar el uso de un estilo de programación funcional en todo el sistema.

Algunos idiomas imperativos tienen un idioma similar. Por ejemplo, una static constvariable en C ++ se inicializa solo una vez, antes de usar su valor, y nunca muta.

Davislor
fuente
3

Depende de cómo lo hagas.

Por lo general, la gente quiere memorizar mutando algún tipo de diccionario de caché. Esto tiene todos los problemas asociados con la mutación impura, como tener que preocuparse por la concurrencia, preocuparse por que el caché crezca demasiado, etc.

Sin embargo, puede memorizar sin mutación de memoria impura. Un ejemplo es en esta respuesta , donde hago un seguimiento externo de los valores memorizados mediante un lengthsargumento.

En el enlace que Robert Harvey proporcionó , la evaluación diferida se usa para evitar efectos secundarios.

Otra técnica que se ve a veces es marcar explícitamente la memorización como un efecto secundario impuro en el contexto de un IOtipo, como con la función de memorización de efecto gatos .

Este último plantea el punto de que a veces el objetivo es encapsular la mutación en lugar de eliminarla. La mayoría de los programadores funcionales lo consideran "suficientemente puro" para hacer que la impureza sea explícita y encapsulada.

Si desea que un término lo diferencie de una función verdaderamente pura, creo que es suficiente decir "memorizado con un diccionario mutable". Eso les permite a las personas saber cómo usarlo de manera segura.

Karl Bielefeldt
fuente
No creo que ninguna de las soluciones más puras resuelva los problemas anteriores: si bien pierde cualquier preocupación de concurrencia, también pierde la posibilidad de que dos llamadas iniciadas simultáneamente collatz(100)y como collatz(200)cooperar. Y IIUIC, el problema con el caché que crece demasiado sigue siendo (¿aunque Haskell puede tener algunos buenos trucos para esto?).
maaartinus
Nota: IOes puro. Todos los métodos impuros IOy los gatos se nombran unsafe. Async.memoizetambién es puro, por lo que no tenemos que conformarnos con "lo suficientemente puro" :)
Samuel
2

Por lo general, una función que devuelve una lista no es pura en absoluto porque requiere la asignación de almacenamiento y, por lo tanto, puede fallar (por ejemplo, al lanzar una excepción, que no es pura). Un lenguaje que tiene tipos de valor y puede representar una lista como un tipo de valor de tamaño acotado puede no tener este problema. Por esta razón, su ejemplo probablemente no sea puro.

En general, si la memorización se puede hacer de una manera libre de fallas (por ejemplo, al tener almacenamiento estáticamente asignado para resultados memorizados y una sincronización interna para controlar el acceso a ellos si el idioma admite hilos), es razonable considerar dicha función puro.

R ..
fuente
0

Puede implementar la memorización sin efectos secundarios utilizando la mónada estatal .

[Mónada de estado] es básicamente una función S => (S, A), donde S es el tipo que representa su estado y A es el resultado que produce la función - Estado de gatos .

En su caso, el estado sería el valor memorizado o nada (es decir, Haskell Maybeo Scala Option[A]). Si el valor memorizado está presente, se devuelve como A; de lo contrario, Ase calcula y se devuelve como el estado de transición y el resultado.

Samuel
fuente