Recuerde: ¡la programación funcional no es programación con funciones! ! !
23.4 Programación funcional
23.4.1 ¿Qué es la programación funcional?
¿Qué es la programación funcional? Si lo preguntas tan claramente, descubrirás que es un concepto que no es fácil de explicar. Muchos veteranos con muchos años de experiencia en el campo de la programación no pueden explicar claramente qué está estudiando la programación funcional. La programación funcional es de hecho un campo desconocido para los programadores que están familiarizados con la programación procedimental. Los conceptos de cierre, continuación y curry nos parecen tan desconocidos que los familiares if, else y while no tienen nada en común. Aunque la programación funcional tiene hermosos prototipos matemáticos que la programación procedimental no puede igualar, es tan misteriosa que sólo aquellos con un doctorado pueden dominarla.
Consejo: Esta sección es un poco difícil, pero no es una habilidad necesaria para dominar JavaScript si no desea utilizar JavaScript para completar las tareas que se realizan en Lisp, o no desea aprender las habilidades esotéricas de. programación funcional, puede omitirla y entrar en el siguiente capítulo de su viaje.
Volviendo a la pregunta, ¿qué es la programación funcional? La respuesta es larga...
La primera ley de la programación funcional: las funciones son de primer tipo.
¿Cómo debería entenderse esta frase en sí? ¿Qué es un verdadero Tipo Uno? Veamos los siguientes conceptos matemáticos:
ecuación binaria F(x, y) = 0, x, y son variables, escríbala como y = f(x), x es un parámetro, y es el valor de retorno, f es de x a y La relación de mapeo se llama función. Si lo hay, G(x, y, z) = 0, o z = g(x, y), g es la relación de mapeo de x, y a z, y también es una función. Si los parámetros xey de g satisfacen la relación anterior y = f(x), entonces obtenemos z = g(x, y) = g(x, f(x)). x) es una función sobre x y un parámetro de la función g. En segundo lugar, g es una función de orden superior que f.
De esta manera, usamos z = g(x, f(x)) para representar la solución asociada de las ecuaciones F(x, y) = 0 y G(x, y, z) = 0, que es una función iterativa. . También podemos expresar g de otra forma, recuerde z = g(x, y, f), de modo que generalicemos la función g a una función de orden superior. En comparación con la anterior, la ventaja de esta última representación es que es un modelo más general, como la solución asociada de T(x,y) = 0 y G(x,y,z) = 0. También se puede expresar de la misma forma (simplemente sea f = t). En este sistema de lenguaje que admite la iteración de convertir la solución de un problema en una función de orden superior, la función se denomina "primer tipo".
Las funciones en JavaScript son claramente de "primer tipo". Aquí hay un ejemplo típico:
Array.prototype.each = función(cierre)
{
devolver this.length ? [cierre(this[0])].concat(this.slice(1).each(closure)): [];
}
Este es realmente un código mágico, que da rienda suelta al encanto del estilo funcional. Solo hay funciones y símbolos en todo el código. Es de forma simple e infinitamente poderoso.
[1,2,3,4].cada(función(x){retorno x * 2}) obtiene [2,4,6,8], mientras que [1,2,3,4].cada(función(x ){return x-1}) obtiene [0,1,2,3].
La esencia de lo funcional y orientado a objetos es que "el Tao sigue a la naturaleza". Si la orientación a objetos es una simulación del mundo real, entonces la expresión funcional es una simulación del mundo matemático. En cierto sentido, su nivel de abstracción es más alto que el de la orientación a objetos, porque los sistemas matemáticos tienen características inherentes que son de naturaleza incomparable. de abstracción.
La segunda ley de la programación funcional: los cierres son el mejor amigo de la programación funcional.
Los cierres, como hemos explicado en capítulos anteriores, son muy importantes para la programación funcional. Su característica más importante es que puede acceder directamente al entorno externo desde la capa interna sin pasar variables (símbolos). Esto brinda una gran comodidad a los programas funcionales bajo anidamiento múltiple. Aquí hay un ejemplo:
(función externalFun(x).
{
función de retorno insideFun(y)
{
devolver x * y;
}
})(2)(3);
La tercera ley de la programación funcional: las funciones pueden ser Currying.
¿Qué es el curry? Es un concepto interesante. Comencemos con las matemáticas: digamos, considere una ecuación espacial tridimensional F(x, y, z) = 0, si limitamos z = 0, entonces obtenemos F(x, y, 0) = 0, denotado como F '(x,y). Aquí F' es obviamente una nueva ecuación, que representa la proyección bidimensional de la curva espacial tridimensional F(x, y, z) en el plano z = 0. Denotemos y = f(x, z), sea z = 0, obtenemos y = f(x, 0), denotamos como y = f'(x), decimos que la función f' es una solución de Currying de f .
A continuación se proporciona un ejemplo de curry de JavaScript:
función sumar(x, y)
{
if(x!=null && y!=null) devuelve x + y;
de lo contrario si (x! = nulo && y == nulo) función de retorno (y)
{
devolver x + y;
}
else if(x==null && y!=null) función de retorno(x)
{
devolver x + y;
}
}
var a = agregar(3, 4);
var b = agregar(2);
var c = b(10);
En el ejemplo anterior, b=add(2) da como resultado una función de Curry de add(), que es una función del parámetro y cuando x = 2. Tenga en cuenta que también se usa arriba. de cierres.
Curiosamente, podemos generalizar Currying para cualquier función, por ejemplo:
función Foo(x, y, z, w)
{
var argumentos = argumentos
si (Foo.length < args.length)
función de retorno()
{
devolver
args.callee.apply(Array.apply([], args).concat(Array.apply([], argumentos)));
}
demás
devolver x + y – z * w;
}
La cuarta ley de la programación funcional: evaluación retrasada y continuación.
//TODO: Piénsalo de nuevo aquí
23.4.2 Ventajas de
las pruebas unitarias
de programación funcionalCada símbolo de programación funcional estricta es una referencia a una cantidad directa o resultado de expresión, y ninguna función tiene efectos secundarios. Porque el valor nunca se modifica en algún lugar y ninguna función modifica una cantidad fuera de su alcance que es utilizada por otras funciones (como miembros de clase o variables globales). Esto significa que el resultado de la evaluación de una función es solo su valor de retorno, y lo único que afecta su valor de retorno son los parámetros de la función.
Este es el sueño húmedo de cualquier probador de unidades. Para cada función en el programa bajo prueba, solo necesita preocuparse por sus parámetros, sin tener que considerar el orden de las llamadas a funciones o configurar cuidadosamente el estado externo. Todo lo que tienes que hacer es pasar parámetros que representen casos extremos. Si cada función del programa pasa la prueba unitaria, tendrá una confianza considerable en la calidad del software. Pero la programación imperativa no puede ser tan optimista. En Java o C++, no basta con comprobar el valor de retorno de una función; también debemos verificar el estado externo que la función puede haber modificado.
Depuración
Si un programa funcional no se comporta de la manera esperada, la depuración es pan comido. Debido a que los errores en los programas funcionales no dependen de rutas de código no relacionadas con ellos antes de su ejecución, los problemas que encuentre siempre se pueden reproducir. En los programas imperativos, los errores aparecen y desaparecen, porque la función de la función depende de los efectos secundarios de otras funciones, y es posible que busque durante mucho tiempo en direcciones no relacionadas con la aparición del error, pero sin ningún resultado. Este no es el caso con los programas funcionales: si el resultado de una función es incorrecto, no importa qué más ejecute antes, la función siempre devolverá el mismo resultado incorrecto.
Una vez que vuelva a crear el problema, encontrar la causa raíz no le resultará difícil e incluso puede hacerle feliz. Interrumpa la ejecución de ese programa y examine la pila. Al igual que con la programación imperativa, se le presentan los parámetros de cada llamada a función en la pila. Pero en programas imperativos estos parámetros no son suficientes. Las funciones también dependen de variables miembro, variables globales y el estado de la clase (que a su vez depende de muchas de estas). En programación funcional, una función sólo depende de sus parámetros, ¡y esa información está justo delante de tus ojos! Además, en un programa imperativo, simplemente verificar el valor de retorno de una función no puede garantizar que la función esté funcionando correctamente. Debe verificar el estado de docenas de objetos fuera del alcance de esa función para confirmar. ¡Con un programa funcional, todo lo que tienes que hacer es mirar su valor de retorno!
Verifique los parámetros y los valores de retorno de la función a lo largo de la pila. Tan pronto como encuentre un resultado irrazonable, ingrese esa función y siga paso a paso hasta encontrar el punto donde se genera el error.
Los programas funcionales paralelos se pueden ejecutar en paralelo sin ninguna modificación. ¡No te preocupes por los puntos muertos y las secciones críticas porque nunca usas candados! Ningún dato en un programa funcional es modificado dos veces por el mismo hilo, y mucho menos por dos hilos diferentes. Esto significa que los subprocesos se pueden agregar simplemente sin pensarlo dos veces y sin causar los problemas tradicionales que afectan a las aplicaciones paralelas.
Si este es el caso, ¿por qué no todo el mundo utiliza la programación funcional en aplicaciones que requieren operaciones altamente paralelas? Bueno, lo están haciendo. Ericsson diseñó un lenguaje funcional llamado Erlang y lo utilizó en conmutadores de telecomunicaciones que requieren escalabilidad y tolerancia a fallas extremadamente altas. Muchas personas también descubrieron las ventajas de Erlang y comenzaron a utilizarlo. Estamos hablando de sistemas de control de telecomunicaciones, que requieren mucha más confiabilidad y escalabilidad que un sistema típico diseñado para Wall Street. De hecho, el sistema Erlang no es confiable ni extensible, JavaScript sí lo es. Los sistemas Erlang son simplemente sólidos como una roca.
La historia del paralelismo no termina ahí. Incluso si su programa tiene un solo subproceso, un compilador de programa funcional aún puede optimizarlo para ejecutarlo en múltiples CPU. Mire el siguiente código:
String s1 = algoLongOperation1();
Cadena s2 = algoLongOperation2();
String s3 = concatenate(s1, s2);
En un lenguaje de programación funcional, el compilador analiza el código para identificar funciones que pueden consumir mucho tiempo y crean cadenas s1 y s2, y luego las ejecuta en paralelo. Esto no es posible en lenguajes imperativos, donde cada función puede modificar el estado fuera del alcance de la función y las funciones posteriores pueden depender de estas modificaciones. En los lenguajes funcionales, analizar funciones automáticamente e identificar candidatos adecuados para la ejecución paralela es tan simple como la incorporación automática de funciones. En este sentido, la programación de estilo funcional está "preparada para el futuro" (incluso si no me gusta usar términos de la industria, esta vez haré una excepción). Los fabricantes de hardware ya no podían hacer que las CPU funcionaran más rápido, por lo que aumentaron la velocidad de los núcleos del procesador y lograron cuadriplicar la velocidad debido al paralelismo. Por supuesto, también se olvidaron de mencionar que el dinero extra que gastamos sólo se utilizó en software para resolver problemas paralelos. Una pequeña proporción de software imperativo y software 100% funcional pueden ejecutarse directamente en paralelo en estas máquinas.
La implementación en caliente de código
solía requerir la instalación de actualizaciones en Windows y reiniciar la computadora era inevitable, y más de una vez, incluso si se instalaba una nueva versión del reproductor multimedia. Windows XP mejoró enormemente esta situación, pero aún no es ideal (hoy ejecuté Windows Update en el trabajo y ahora siempre aparece un ícono molesto en la bandeja a menos que reinicie la máquina). Los sistemas Unix siempre se han ejecutado en un modo mejor. Al instalar actualizaciones, solo es necesario detener los componentes relacionados con el sistema, en lugar de todo el sistema operativo. Aun así, esto sigue siendo insatisfactorio para una aplicación de servidor a gran escala. Los sistemas de telecomunicaciones deben estar operativos el 100% del tiempo porque si falla el marcado de emergencia mientras se actualiza el sistema, se podrían perder vidas. No hay razón para que las empresas de Wall Street deban cerrar sus servicios durante el fin de semana para instalar actualizaciones.
La situación ideal es actualizar el código relevante sin detener ningún componente del sistema. Esto es imposible en un mundo imperativo. Tenga en cuenta que cuando el tiempo de ejecución carga una clase Java y anula una nueva definición, todas las instancias de esta clase no estarán disponibles porque se pierde su estado guardado. Podríamos comenzar a escribir un tedioso código de control de versiones para resolver este problema, luego serializar todas las instancias de esta clase, destruir estas instancias, luego recrear estas instancias con la nueva definición de esta clase y luego cargar los datos previamente serializados y esperar que la carga se complete. El código transferirá correctamente esos datos a la nueva instancia. Además de esto, el código de transferencia debe reescribirse manualmente para cada actualización y se debe tener mucho cuidado para evitar romper las interrelaciones entre objetos. La teoría es simple, pero la práctica no es fácil.
Para los programas funcionales, todos los estados, es decir, los parámetros pasados a la función, se guardan en la pila, lo que hace que la implementación en caliente sea muy sencilla. De hecho, todo lo que tenemos que hacer es hacer una diferencia entre el código de trabajo y la nueva versión, y luego implementar el nuevo código. ¡El resto se hará automáticamente mediante una herramienta de lenguaje! Si crees que esto es una historia de ciencia ficción, piénsalo de nuevo. Durante años, los ingenieros de Erlang han estado actualizando sus sistemas en ejecución sin interrumpirlos.
Razonamiento y optimización asistidos por máquinas
Una propiedad interesante de los lenguajes funcionales es que se pueden razonar matemáticamente. Debido a que un lenguaje funcional es solo una implementación de un sistema formal, todas las operaciones realizadas en papel se pueden aplicar a programas escritos en este lenguaje. Los compiladores pueden utilizar la teoría matemática para transformar un fragmento de código en un código equivalente pero más eficiente [7]. Las bases de datos relacionales llevan años sufriendo este tipo de optimización. No hay ninguna razón por la que esta técnica no pueda aplicarse al software normal.
Además, puede utilizar estas técnicas para demostrar que partes de su programa son correctas y tal vez incluso crear herramientas para analizar su código y generar automáticamente casos extremos para pruebas unitarias. Esta funcionalidad no tiene ningún valor para un sistema robusto, pero si está diseñando un marcapasos o un sistema de control de tráfico aéreo, esta herramienta es indispensable. Si las aplicaciones que escribe no son tareas centrales de la industria, este tipo de herramienta también puede brindarle una ventaja sobre sus competidores.
23.4.3 Desventajas de la programación funcional
Efectos secundarios de los cierres
En la programación funcional no estricta, los cierres pueden anular el entorno externo (ya lo hemos visto en el capítulo anterior), lo que trae efectos secundarios, y cuando dichos efectos secundarios ocurren con frecuencia Y cuando El entorno en el que se ejecuta el programa cambia con frecuencia y los errores se vuelven difíciles de rastrear.
//TODO:
forma recursiva
Aunque la recursividad suele ser la forma de expresión más concisa, no es tan intuitiva como los bucles no recursivos.
//TODO:
La debilidad del valor retrasado
//TODO: