Beneficios de la función pura

82

Hoy estaba leyendo sobre la función pura, me confundí con su uso:

Se dice que una función es pura si devuelve el mismo conjunto de valores para el mismo conjunto de entradas y no tiene efectos secundarios observables.

por ejemplo, strlen()es una función pura mientras que rand()es impura.

__attribute__ ((pure)) int fun(int i)
{
    return i*i;
}

int main()
{
    int i=10;
    printf("%d",fun(i));//outputs 100
    return 0;
}

http://ideone.com/33XJU

El programa anterior se comporta de la misma manera que en ausencia de puredeclaración.

¿Cuáles son los beneficios de declarar una función como pure[si no hay cambios en la salida]?

Duende Verde
fuente
7
Sí, mire el ensamblaje generado.
Philip Kendall
4
No creo que esta definición de pureza sea correcta printf; por ejemplo, calificaría (llamarla dos veces con los mismos argumentos arroja el mismo valor de retorno), pero no es pura.
tdammers
14
@tdammers: De hecho, le falta el ...and no side-effects...papel.
Frerich Raabe
2
@Ben: ¿de dónde viene la entropía? Estamos tratando con máquinas (teóricamente) deterministas aquí, la única forma de obtener verdadera entropía es a partir de fuentes externas, lo que significa efectos secundarios. Por supuesto, podríamos permitir que los lenguajes de programación definan funciones no deterministas, pretendiendo que los efectos secundarios técnicos no están ahí y que las funciones son realmente no deterministas; pero si hacemos eso, se pierden la mayoría de los beneficios prácticos de rastrear la pureza.
tdammers
3
tdammers es correcto: la definición de puro dada anteriormente es incorrecta. Puro significa que la salida depende solo de las entradas a la función; además, no debe haber efectos secundarios observables. "Misma salida para la misma entrada" es un resumen muy inexacto de esos requisitos. en.wikipedia.org/wiki/Pure_function
Dancrumb

Respuestas:

145

pure le permite al compilador saber que puede hacer ciertas optimizaciones sobre la función: imagina un poco de código como

for (int i = 0; i < 1000; i++)
{
    printf("%d", fun(10));
}

Con una función pura, el compilador puede saber que necesita evaluar fun(10)una vez y solo una vez, en lugar de 1000 veces. Para una función compleja, es una gran victoria.

Philip Kendall
fuente
Es decir, puede utilizar la memorización de forma segura
Joel Coehoorn
@mob ¿A qué te refieres? Por qué no?
Konrad Rudolph
15
Porque puede modificar la cadena (secuencia de caracteres a partir de alguna dirección) sin modificar la entrada (el puntero a la dirección donde comienza la cadena), es decir, no puede memorizarla. Solo sería una función pura en un lenguaje con cadenas inmutables (Java, por ejemplo).
mafia
5
@KonradRudolph: Imagina una cadena de 1000 de longitud. Llámalo strlen. Entonces otra vez. Lo mismo, ¿sí? Ahora modifique el segundo carácter para que sea \0. ¿ strlenTodavía devuelve 1000 ahora? La dirección inicial es la misma (== la entrada es la misma) pero la función ahora devuelve un valor diferente.
Mike Bailey
5
@mob Esa es una buena objeción, obviamente tienes razón. Me engañó el hecho de que incluso los libros afirman que strlen(en GCC / glibc) es de hecho puro. Pero una mirada a la implementación de glibc mostró que esto estaba mal.
Konrad Rudolph
34

Cuando dice que una función es 'pura', está garantizando que no tiene efectos secundarios visibles externamente (y como dice un comentario, si miente, pueden suceder cosas malas). Saber que una función es 'pura' tiene beneficios para el compilador, que puede usar este conocimiento para realizar ciertas optimizaciones.

Esto es lo que dice la documentación de GCC sobre el pureatributo:

puro

Muchas funciones no tienen ningún efecto excepto el valor de retorno y su valor de retorno depende solo de los parámetros y / o variables globales. Una función de este tipo puede estar sujeta a la eliminación de subexpresiones comunes y la optimización del bucle, tal como lo estaría un operador aritmético. Estas funciones deben declararse con el atributo pure. Por ejemplo,

          int square (int) __attribute__ ((pure));

La respuesta de Philip ya muestra cómo saber que una función es 'pura' puede ayudar con las optimizaciones de bucle.

Aquí hay uno para la eliminación de subexpresión común (dado fooes puro):

a = foo (99) * x + y;
b = foo (99) * x + z;

Puede llegar a ser:

_tmp = foo (99) * x;
a = _tmp + y;
b = _tmp + z;
ArjunShankar
fuente
3
No estoy seguro de si alguno hace esto, pero las funciones puras también permiten al compilador reordenar cuando se llama a la función, en caso de que considere que el reordenamiento sería beneficioso. Cuando existe la posibilidad de efectos secundarios, el compilador debe ser más conservador.
mpdonadio
@MPD - Sí, eso suena razonable. Y dado que una callinstrucción es un cuello de botella para las CPU superescalares, la ayuda del compilador puede ayudar.
ArjunShankar
Recuerdo vagamente haber usado un compilador DSP hace varios años que usaría esta técnica para obtener valores de retorno tarde o temprano. Esto le permitió minimizar las paradas de la tubería.
mpdonadio
1
¿Se podría precalcular "foo (99)" ya que 99 es una constante y foo siempre devolverá el mismo resultado? ¿Quizás en una especie de compilación de dos etapas?
Markwatson
1
@markwatson: no estoy seguro. Puede haber casos en los que simplemente no sea posible. por ejemplo, si fooes parte de otra unidad de compilación (otro archivo C), o en una biblioteca precompilada. En ambos casos, el compilador no sabrá lo que foohace y no puede pre-calcular.
ArjunShankar
29

Además de los posibles beneficios en tiempo de ejecución, es mucho más fácil razonar sobre una función pura cuando se lee el código. Además, es mucho más fácil probar una función pura, ya que sabe que el valor de retorno solo depende de los valores de los parámetros.

Frerich Raabe
fuente
3
+1, su punto sobre las pruebas es interesante. No es necesario instalar ni desmontar.
ArjunShankar
15

Una función no pura

int foo(int x, int y) // possible side-effects

es como una extensión de una función pura

int bar(int x, int y) // guaranteed no side-effects

en el que tiene, además de los argumentos de función explícita x, y, el resto del universo (o cualquier cosa con la que su computadora pueda comunicarse) como una entrada potencial implícita. Del mismo modo, además del valor de retorno entero explícito, cualquier cosa que su computadora pueda escribir es implícitamente parte del valor de retorno.

Debe quedar claro por qué es mucho más fácil razonar sobre una función pura que sobre una no pura.

Giorgio
fuente
1
+1: Usar el universo como entrada potencial es una forma muy agradable de explicar la diferencia entre puro y no puro.
ArjunShankar
de hecho, esta es la idea detrás de las mónadas.
Kristopher Micinski
7

Solo como complemento, me gustaría mencionar que C ++ 11 codifica las cosas de alguna manera usando la palabra clave constexpr. Ejemplo:

#include <iostream>
#include <cstring>

constexpr unsigned static_strlen(const char * str, unsigned offset = 0) {
        return (*str == '\0') ? offset : static_strlen(str + 1, offset + 1);
}

constexpr const char * str = "asdfjkl;";

constexpr unsigned len = static_strlen(str); //MUST be evaluated at compile time
//so, for example, this: int arr[len]; is legal, as len is a constant.

int main() {
    std::cout << len << std::endl << std::strlen(str) << std::endl;
    return 0;
}

Las restricciones en el uso de constexpr hacen que la función sea demostrablemente pura. De esta manera, el compilador puede optimizar de manera más agresiva (¡solo asegúrese de usar la recursividad de cola, por favor!) Y evaluar la función en tiempo de compilación en lugar de en tiempo de ejecución.

Entonces, para responder a su pregunta, es que si está usando C ++ (sé que dijo C, pero están relacionados), escribir una función pura en el estilo correcto le permite al compilador hacer todo tipo de cosas interesantes con la función: -)

Robert Mason
fuente
4

En general, las funciones puras tienen 3 ventajas sobre las funciones impuras que el compilador puede aprovechar:

Almacenamiento en caché

Digamos que tiene una función pura fque se llama 100000 veces, ya que es determinista y depende solo de sus parámetros, el compilador puede calcular su valor una vez y usarlo cuando sea necesario

Paralelismo

Las funciones puras no leen ni escriben en ninguna memoria compartida y, por lo tanto, pueden ejecutarse en subprocesos separados sin consecuencias inesperadas.

Pasando por referencia

Una función f(struct t)obtiene su argumento tpor valor y, por otro lado, el compilador puede pasar tpor referencia a fsi se declara como pura mientras garantiza que el valor de tno cambiará y tendrá ganancias de rendimiento.


Además de las consideraciones sobre el tiempo de compilación, las funciones puras se pueden probar con bastante facilidad: simplemente llámelas.

No es necesario construir objetos o simular conexiones a bases de datos / sistema de archivos.

Uri Goren
fuente