¿Ventajas de la programación sin estado?

132

Recientemente he estado aprendiendo sobre programación funcional (específicamente Haskell, pero también he visto 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 capacidad de guardar el estado de una manera fácil (no considero que las mónadas de Haskell sean "fáciles").

¿Vale la pena continuar aprendiendo Haskell (u otro lenguaje puramente funcional) en profundidad? ¿Es la programación funcional o sin estado realmente más productiva que procesal? ¿Es probable que continúe usando Haskell u otro lenguaje funcional más tarde, o debería aprenderlo solo para entenderlo?

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

Sasha Chedygov
fuente

Respuestas:

168

Lea la programación funcional en pocas palabras .

La programación sin estado tiene muchas ventajas, entre las cuales se encuentra un código dramáticamente multiproceso y concurrente. 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 subproceso mute el valor del estado compartido entre dos subprocesos, por lo que elimina toda una clase de errores de subprocesos múltiples relacionados con las condiciones de carrera. Como no hay condiciones de carrera, tampoco hay razón para usar bloqueos, por lo que la inmutabilidad elimina otra clase completa de errores relacionados con los puntos muertos también.

Esa es la gran razón por la cual la programación funcional es importante, y probablemente la mejor para saltar al tren de programación funcional. También hay muchos otros beneficios, incluida la depuración simplificada (es decir, las funciones son puras y no mutan el estado en otras partes de una aplicación), código más conciso y expresivo, menos 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 más agresivamente su código.

Julieta
fuente
55
¡Secundo esto! Creo que la programación funcional se usará mucho más en el futuro debido a su idoneidad para la programación paralela.
Ray Hidayat
@Ray: ¡También agregaría programación distribuida!
Anton Tykhyy
La mayor parte de eso es cierto, excepto para la depuración. En general, es más difícil en Haskell, porque no tienes una pila de llamadas reales, solo una pila de coincidencia de patrones. Y es mucho más difícil predecir cómo terminará su código.
hasufell
3
Además, la programación funcional no se trata realmente de "sin estado". La recursión ya es un estado implícito (local) y es lo principal que hacemos en Haskell. Eso queda claro una vez que implementa algunos algoritmos no triviales en haskell idiomático (por ejemplo, material de geometría computacional) y se divierte depurando esos.
hasufell
2
No me gusta equiparar apátridas con FP. Muchos programas de FP están llenos de estado, simplemente existe en un cierre en lugar de un objeto.
mikemaccana
46

Cuantas más piezas de su programa sean apátridas, más formas habrá de armar piezas sin que nada se rompa . El poder del paradigma sin estado no reside en la apatridia (o la pureza) per se , sino en la capacidad que le brinda para escribir funciones poderosas y reutilizables y combinarlas.

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

Usted será pegotes más productivo, especialmente si tienes que elegir un lenguaje funcional que también tiene tipos de datos algebraicos y la coincidencia de patrones (Caml, SML, Haskell).

Norman Ramsey
fuente
¿Los mixins tampoco proporcionarían código reutilizable de manera similar con OOP? No abogo por OOP solo tratando de entender las cosas por mí mismo.
mikemaccana
20

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

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

Por ejemplo, aunque F # esté estáticamente tipado (todos los tipos se resuelven en tiempo de compilación), la inferencia de tipos determina qué tipos tiene, por lo que no tiene que decirlo. Y si no puede resolverlo, automáticamente convierte su función / clase / lo que sea genérico. Así que nunca tienes que escribir ningún genérico, todo es automático. Creo 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 #, encuentro que realmente extraño esta inferencia de tipo, nunca te das cuenta de lo molesto que es hasta que ya no tienes que hacerlo.

También en F #, en lugar de escribir bucles, se llaman funciones. Es un cambio sutil, pero significativo, porque ya no tienes que pensar en la construcción del bucle. Por ejemplo, aquí hay una pieza de código que pasaría y coincidiría con algo (no recuerdo qué, es de un proyecto de rompecabezas de Euler):

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

Me doy cuenta de que hacer un filtro y luego un mapa (que es una conversión de cada elemento) en C # sería bastante simple, pero hay que pensar en un nivel inferior. En particular, tendrías que escribir el ciclo en sí y tener tu propia declaración if explícita, y ese tipo de cosas. Desde que aprendí F #, me di cuenta de que me resulta más fácil codificar de manera funcional, donde si quieres filtrar, escribes "filter", y si quieres mapear, escribes "map", en lugar de implementar Cada uno de los detalles.

También me encanta el operador |>, que creo que separa F # de ocaml, y posiblemente otros lenguajes funcionales. Es el operador de tubería, le permite "canalizar" la salida de una expresión en la entrada de otra expresión. Hace que el código siga cómo pienso más. Al igual que en el fragmento de código anterior, que dice: "tome la secuencia de factores, filtre y luego asigne". Es un nivel de pensamiento muy alto, que no se obtiene en un lenguaje de programación imperativo porque estás muy ocupado escribiendo el bucle y las declaraciones if. Es lo único que más extraño cada vez que voy a otro idioma.

Entonces, en general, aunque puedo programar en C # y F #, me resulta más fácil usar F # porque puedes 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 # se puede escribir imperativamente, así que aquí hay un ejemplo directo de cómo puede tener un estado mutable en F #:

let mutable x = 5
for i in 1..10 do
    x <- x + i
Ray Hidayat
fuente
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áctico para b (a, p1, p2). Combina esto con la asociatividad correcta y lo tienes.
Anton Tykhyy
2
Es cierto que 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 tipos y |> no son programación funcional per se, ciertamente diría que "van con el territorio". Al menos en general.
Ray Hidayat
|> es solo otra función infija de orden superior, en este caso un operador de aplicación de función. Definir sus propios operadores infix 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.
Norman Ramsey
15

Considere todos los errores difíciles que ha pasado mucho tiempo depurando.

Ahora, ¿cuántos de esos errores se debieron a "interacciones no intencionadas" entre dos componentes separados de un programa? (Casi todos los errores de subprocesos tienen esta forma: razas que implican escribir datos compartidos, puntos muertos, ... 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 postularía que al menos 1 de cada 3 'errores duros' entran en esta categoría.

Ahora, si cambias a programación sin estado / inmutable / pura, todos esos errores desaparecen. En su lugar, se le presentan algunos desafíos nuevos (por ejemplo, cuando lo hace querer que diferentes módulos para interactuar con el medio ambiente), pero en un lenguaje como Haskell, esas interacciones quedan materializadas de forma explícita en el sistema de tipos, lo que significa que se puede simplemente mirar el tipo de una 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' de la OMI. En un mundo ideal, todos diseñaríamos API fabulosas 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 no intencionadas / implícitas / detrás de escena entre los componentes.

Brian
fuente
66
Alguien dijo una vez que sobrescribir un valor mutable significa que está recolectando basura / liberando explícitamente el valor anterior. En algunos casos, otras partes del programa no se realizaron utilizando ese valor. Cuando los valores no pueden ser mutados, esta clase de errores también desaparece.
shapr
8

Una ventaja de las funciones sin estado es que permiten el cálculo previo o el almacenamiento en caché de los valores de retorno de la función. Incluso algunos compiladores de C le permiten marcar explícitamente funciones como apátridas para mejorar su optimizabilidad. Como muchos otros han notado, las funciones sin estado son mucho más fáciles de paralelizar.

Pero la eficiencia no es la única preocupación. Una función pura es más fácil de probar y depurar ya que cualquier cosa que la afecte se indica explícitamente. Y cuando se programa en un lenguaje funcional, uno tiene la costumbre de hacer la menor cantidad de funciones "sucias" (con E / S, etc.) como sea posible. Separar las cosas con estado de esta manera es una buena manera de diseñar programas, incluso en lenguajes no tan funcionales.

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

Artelius
fuente
Esa primera parte es un punto realmente interesante, nunca había pensado en eso antes. ¡Gracias!
Sasha Chedygov
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 el
6

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

Zifre
fuente
Sí, definitivamente lo he investigado. El modelo de concurrencia de Erlang en particular es muy intrigante. Sin embargo, en este momento no me importa tanto la concurrencia como la productividad. ¿Hay una bonificación de productividad por programar sin estado?
Sasha Chedygov
2
@musicfreak, no, no hay un bono de productividad. Pero como nota, los lenguajes FP modernos aún le permiten usar el estado si realmente lo necesita.
Desconocido
De Verdad? ¿Puedes dar un ejemplo de estado en un lenguaje funcional, solo para que pueda ver cómo se hace?
Sasha Chedygov
Echa un vistazo a la mónada estatal en Haskell - book.realworldhaskell.org/read/monads.html#x_NZ
rampion
44
@ Desconocido: no estoy de acuerdo. La programación sin estado reduce la aparición de errores debidos a interacciones imprevistas / involuntarias de diferentes componentes. También fomenta un mejor diseño (más reutilización, separación de mecanismos y políticas, y ese tipo de cosas). No siempre es apropiado para la tarea en cuestión, pero en algunos casos realmente brilla.
Artelius el
6

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

Podría haber muchos datos de usuario que no desea almacenar en el lado del cliente por razones de seguridad, por ejemplo. En este caso, debe almacenarlo en el lado del servidor. Puede usar 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 esté siempre dirigido a la misma instancia.

Los equilibradores de carga a menudo tienen la capacidad de tener 'sesiones fijas' donde el equilibrador de carga sabe de algún modo a qué servidor enviar la solicitud de los usuarios. Sin embargo, esto no es ideal, por ejemplo, significa que cada vez que reinicia 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 un montón de excelentes productos nosql disponibles para esto (redis, mongo, elasticsearch, memcached). De esta manera, los servidores web no tienen estado, pero aún tiene un estado del lado del servidor y la disponibilidad de este estado se puede administrar eligiendo la configuración correcta del almacén de datos. Estos almacenes de datos generalmente tienen una gran redundancia, por lo que casi siempre debería ser posible realizar cambios en su aplicación web e incluso en el almacén de datos sin afectar a los usuarios.

shmish111
fuente