2009-05-10 12 views
107

Recientemente he estado aprendiendo sobre programación funcional (específicamente Haskell, pero también he asistido a tutoriales sobre Lisp y Erlang). Si bien encontré los conceptos muy esclarecedores, todavía no veo el lado práctico del concepto de "sin efectos secundarios". ¿Cuáles son las ventajas prácticas de esto? Estoy tratando de pensar en la mentalidad funcional, pero hay algunas situaciones que parecen demasiado complejas sin la posibilidad de guardar el estado de una manera fácil (no considero que las mónadas de Haskell sean 'fáciles').¿Ventajas de la programación sin estado?

¿Vale la pena continuar aprendiendo Haskell (u otro lenguaje puramente funcional) en profundidad? ¿La programación funcional o sin estado es más productiva que la de procedimiento? ¿Es probable que continuaré usando Haskell u otro lenguaje funcional más adelante, o debo aprenderlo solo para entenderlo?

Me importa menos el rendimiento que la productividad. Así que principalmente estoy preguntando si seré más productivo en un lenguaje funcional que en un procedimiento/orientado a objetos/lo que sea.

Respuesta

139

Lea Functional Programming in a Nutshell.

Hay muchas ventajas en la programación sin estado, entre las que se encuentra dramáticamente código multiproceso y simultáneo. Para decirlo sin rodeos, el estado mutable es enemigo del código multiproceso. Si los valores son inmutables por defecto, los programadores no tienen que preocuparse de que un hilo mute el valor del estado compartido entre dos hilos, por lo que elimina una clase completa de errores de subprocesamiento múltiple relacionados con las condiciones de carrera. Como no hay condiciones de carrera, tampoco hay motivos para usar bloqueos, por lo que la inmutabilidad elimina también toda otra clase de errores relacionados con los bloqueos.

Esa es la razón principal por la cual la programación funcional es importante, y probablemente la mejor para saltar en el tren de programación funcional. También hay muchos otros beneficios, incluida la depuración simplificada (es decir, las funciones son puras y no cambian el estado en otras partes de una aplicación), un código más escueto y expresivo, menos un código repetitivo en comparación con los idiomas que dependen en gran medida de los patrones de diseño y el compilador puede optimizar su código de forma más agresiva.

+0

¡Bien dicho y al punto! –

+3

¡En segundo lugar! Creo que la programación funcional se usará mucho más ampliamente en el futuro debido a su idoneidad para la programación paralela. –

+0

@Ray: ¡También agregaría programación distribuida! –

3

Sin estado, es muy fácil paralelizar automáticamente su código (ya que las CPU están hechas con más y más núcleos, esto es muy importante).

+0

Sí, definitivamente he investigado eso. El modelo de concurrencia de Erlang en particular es muy intrigante. Sin embargo, en este punto, realmente no me importa tanto la simultaneidad como la productividad. ¿Hay una bonificación de productividad de la programación sin estado? –

+2

@musicfreak, no, no hay una bonificación de productividad. Pero como nota, los modernos lenguajes de FP aún le permiten usar el estado si realmente lo necesita. – Unknown

+0

¿De verdad? ¿Puedes dar un ejemplo de estado en un lenguaje funcional, solo para que pueda ver cómo se hace? –

3

Una de las ventajas de las funciones sin estado es que permiten el precalculado o el almacenamiento en caché de los valores de retorno de la función. Incluso algunos compiladores C le permiten marcar explícitamente funciones como sin estado para mejorar su capacidad de optimización. Como muchos otros han notado, las funciones sin estado son mucho más fáciles de paralelar.

Pero la eficiencia no es la única preocupación. Una función pura es más fácil de probar y depurar ya que todo lo que la afecta está explícitamente establecido. Y cuando se programa en un lenguaje funcional, uno adquiere el hábito de hacer tan pocas funciones "sucias" (con E/S, etc.) como sea posible. Separar las cosas con estado de esta manera es una buena forma de diseñar programas, incluso en lenguajes no tan funcionales.

Los lenguajes funcionales pueden tardar un tiempo en "obtener", y es difícil de explicar a alguien que no ha pasado por ese proceso. Pero la mayoría de las personas que persisten lo suficiente finalmente se dan cuenta de que el alboroto vale la pena, incluso si no terminan usando mucho los lenguajes funcionales.

+0

Esa primera parte es un punto realmente interesante, nunca antes había pensado en eso. ¡Gracias! –

+1

@musicfreak: lee sobre cómo memorizar. –

+0

Supongamos que tiene 'sin (PI/3)' en su código, donde PI es una constante, el compilador podría evaluar esta función * en tiempo de compilación * e incrustar el resultado en el código generado. – Artelius

36

Cuantas más piezas de su programa son apátridas, más formas hay de juntar piezas sin tener nada que romper.El poder del paradigma sin estado radica no en la apatridia (o pureza) per se, sino la capacidad que le brinda para escribir poderosas funciones reutilizables y combinarlas.

Puede encontrar un buen tutorial con muchos ejemplos en el documento de John Hughes Why Functional Programming Matters (PDF).

Serás gobs más productivo, especialmente si eliges un lenguaje funcional que también tenga tipos de datos algebraicos y coincidencia de patrones (Caml, SML, Haskell).

+0

¿Las mezclas tampoco proporcionarían código reutilizable de manera similar con OOP? No defiendo OOP simplemente tratando de entender las cosas por mí mismo. – mikemaccana

17

Muchas de las otras respuestas se han centrado en el lado del rendimiento (paralelismo) de la programación funcional, que creo que es muy importante. Sin embargo, usted específicamente preguntó acerca de la productividad, como en, ¿puede programar lo mismo más rápido en un paradigma funcional que en un paradigma imperativo.

Encuentro (por experiencia personal) que la programación en F # coincide con la forma en que pienso mejor, y por eso es más fácil. Creo que esa es la mayor diferencia. He programado tanto en F # como en C#, y hay mucho menos "pelear el idioma" en F #, lo cual me encanta. No tiene que pensar en los detalles en F #. Aquí hay algunos ejemplos de lo que descubrí que realmente disfruto.

Por ejemplo, aunque F # está tipado estáticamente (todos los tipos se resuelven en tiempo de compilación), la inferencia tipo determina qué tipos tiene, por lo que no tiene que decirlo. Y si no puede resolverlo, automáticamente hace que su función/clase/lo que sea sea genérica. Entonces nunca tienes que escribir ningún genérico, es automático. Encuentro que eso significa que paso más tiempo pensando en el problema y menos cómo implementarlo. De hecho, cada vez que vuelvo a C#, descubro que realmente extraño esta inferencia de tipo, nunca te das cuenta de lo distraída que es hasta que ya no necesites hacerlo.

También en F #, en lugar de escribir bucles, llama a funciones. Es un cambio sutil, pero significativo, porque ya no tienes que pensar en la construcción del lazo. Por ejemplo, aquí hay un trozo de código que ir a través y que coincida con algo (no puedo recordar lo que, es de un proyecto Euler rompecabezas):

let matchingFactors = 
    factors 
    |> Seq.filter (fun x -> largestPalindrome % x = 0) 
    |> Seq.map (fun x -> (x, largestPalindrome/x)) 

Soy consciente de que haciendo un filtro a continuación, un mapa (que es una conversión de cada elemento) en C# sería bastante simple, pero debes pensar en un nivel inferior. Particularmente, tendría que escribir el bucle en sí mismo, y tener su propia declaración explícita si, y ese tipo de cosas. Desde que aprendí F #, me di cuenta de que me resulta más fácil codificar de forma funcional, donde si quieres filtrar, escribes "filter", y si quieres hacer un mapa, escribes "map", en lugar de implementar cada uno de los detalles.

También me gusta el operador |>, que creo que separa F # de ocaml, y posiblemente otros lenguajes funcionales. Es el operador de tuberías, te permite "conectar" la salida de una expresión a la entrada de otra expresión. Hace que el código siga cómo pienso más. Como en el fragmento de código anterior, eso significa: "tomar la secuencia de factores, filtrarla y luego asignarla". Es un nivel muy alto de pensamiento, que no se obtiene en un lenguaje de programación imperativo porque estás tan ocupado escribiendo las instrucciones loop y if. Es lo que más extraño cuando entro a otro idioma.

Por lo general, aunque puedo programar en C# y F #, me resulta más fácil usar F # porque se puede pensar en un nivel superior. Yo diría que debido a que los detalles más pequeños se eliminan de la programación funcional (al menos en F #), soy más productivo.

Editar: Vi en uno de los comentarios que solicitó un ejemplo de "estado" en un lenguaje de programación funcional.F # puede escribirse obligatoriamente, por lo que aquí es un ejemplo directo de cómo se puede tener estado mutable en Fa #:

let mutable x = 5 
for i in 1..10 do 
    x <- x + i 
+1

Estoy de acuerdo con su publicación en general, pero |> no tiene nada que ver con la programación funcional per se. En realidad, 'a |> b (p1, p2)' es solo azúcar sintáctica para 'b (a, p1, p2)'. Asocia esto con la asociatividad correcta y lo tienes. –

+2

Cierto, debo reconocer que probablemente gran parte de mi experiencia positiva con F # tiene más que ver con F # que con la programación funcional. Pero aún así, existe una fuerte correlación entre los dos, y aunque cosas como la inferencia de tipo y |> no son programación funcional en sí misma, ciertamente diría que "van con el territorio". Al menos en general. –

+0

|> es simplemente otra función de infijo de orden superior, en este caso un operador de aplicación de función. Definir sus propios operadores de infijo de orden superior es * definitivamente * una parte de la programación funcional (a menos que sea un Schemer). Haskell tiene su $ que es el mismo excepto que la información en la tubería fluye de derecha a izquierda. –

11

en cuenta todos los errores difíciles que han pasado un largo tiempo de depuración.

Ahora, ¿cuántos de esos errores se debieron a "interacciones involuntarias" entre dos componentes separados de un programa? (Casi todos los errores de subprocesos tienen esta forma: carreras que implican la escritura de datos compartidos, interbloqueos, ... Además, es común encontrar bibliotecas que tienen algún efecto inesperado en el estado global, o leer/escribir el registro/entorno, etc.) I supondría que al menos 1 de cada 3 'bichos duros' entran en esta categoría.

Ahora si cambia a la programación sin estado/inmutable/pura, todos esos errores desaparecen. En su lugar, se le presentan algunos desafíos nuevos (por ejemplo, cuando desea querer diferentes módulos para interactuar con el entorno), pero en un lenguaje como Haskell, esas interacciones se reifican explícitamente en el sistema de tipos, lo que significa que puede simplemente mirar el tipo de función y razón sobre el tipo de interacciones que puede tener con el resto del programa.

Esa es la gran victoria de la 'inmutabilidad' IMO. En un mundo ideal, todos diseñaríamos excelentes API e incluso cuando las cosas fueran mutables, los efectos serían locales y bien documentados, y las interacciones "inesperadas" se mantendrían al mínimo. En el mundo real, hay muchas API que interactúan con el estado global de innumerables maneras, y estas son la fuente de los errores más perniciosos. Aspirar a la apatridia es aspirar a deshacerse de las interacciones involuntarias/implícitas/entre bastidores entre los componentes.

+5

Alguien dijo una vez que sobrescribir un valor mutable significa que usted está recogiendo basura explícitamente/liberando el valor anterior. En algunos casos, otras partes del programa no se hicieron usando ese valor. Cuando los valores no pueden ser mutados, esta clase de errores también desaparece. – shapr

2

Las aplicaciones web sin estado son esenciales cuando comienza a tener un mayor tráfico.

Puede haber muchos datos de usuario que no desee almacenar en el lado del cliente por razones de seguridad, por ejemplo. En este caso, debe almacenarlo en el servidor. Puede utilizar la sesión predeterminada de las aplicaciones web, pero si tiene más de una instancia de la aplicación, deberá asegurarse de que cada usuario siempre se dirija a la misma instancia.

Los equilibradores de carga a menudo tienen la capacidad de tener "sesiones adhesivas" donde el equilibrador de carga sabe cómo a qué servidor enviar la solicitud de los usuarios. Sin embargo, esto no es ideal, por ejemplo, significa que cada vez que reinicie su aplicación web, todos los usuarios conectados perderán su sesión.

Un mejor enfoque es almacenar la sesión detrás de los servidores web en algún tipo de almacén de datos, en estos días hay una gran cantidad de excelentes productos nosql disponibles para esto (redis, mongo, elasticsearch, memcached). De esta manera, los servidores web son apátridas, pero usted todavía tiene el lado del servidor del estado y la disponibilidad de este estado se puede gestionar eligiendo la configuración correcta del almacén de datos. Estas tiendas de datos generalmente tienen una gran redundancia por lo que casi siempre es posible realizar cambios en su aplicación web e incluso en el almacén de datos sin afectar a los usuarios.

Cuestiones relacionadas