Los defensores de FP han afirmado que la concurrencia es fácil porque su paradigma evita el estado mutable. No lo entiendo
Imagina que estamos creando un rastreo de mazmorras multijugador (un roguelike) usando FP donde enfatizamos funciones puras y estructuras de datos inmutables. Generamos una mazmorra compuesta de habitaciones, corredores, héroes, monstruos y botín. Nuestro mundo es efectivamente un gráfico objeto de estructuras y sus relaciones. A medida que las cosas cambian, nuestra representación del mundo se modifica para reflejar esos cambios. Nuestro héroe mata una rata, recoge una espada corta, etc.
Para mí, el mundo (realidad actual) lleva esta idea de estado y me falta cómo FP supera esto. A medida que nuestro héroe toma medidas, las funciones modifican el estado del mundo. Parece que cada decisión (IA o humana) debe basarse en el estado del mundo tal como está en el presente. ¿Dónde permitiríamos la concurrencia? No podemos tener múltiples procesos que modifiquen simultáneamente el estado del mundo para que un proceso no base sus resultados en algún estado vencido. Me parece que todo el control debe ocurrir dentro de un solo ciclo de control para que siempre estemos procesando el estado actual representado por nuestro gráfico actual de objetos del mundo.
Claramente, hay situaciones perfectamente adecuadas para la concurrencia (es decir, cuando se procesan tareas aisladas cuyos estados son independientes entre sí).
No puedo ver cómo la concurrencia es útil en mi ejemplo y ese puede ser el problema. Puedo estar tergiversando el reclamo de alguna manera.
¿Alguien puede representar mejor este reclamo?
fuente
Respuestas:
Trataré de insinuar la respuesta. Esta no es una respuesta, solo una ilustración introductoria. La respuesta de @ jk apunta a lo real, las cremalleras.
Imagina que tienes una estructura de árbol inmutable. Desea alterar un nodo insertando un hijo. Como resultado, obtienes un árbol completamente nuevo.
Pero la mayor parte del árbol nuevo es exactamente igual al árbol viejo. Una implementación inteligente reutilizaría la mayoría de los fragmentos de árbol, enrutando punteros alrededor del nodo alterado:
El libro de Okasaki está lleno de ejemplos como este.
Así que supongo que podría alterar razonablemente pequeñas partes de su mundo de juego en cada movimiento (recoger una moneda), y solo cambiar realmente pequeñas partes de la estructura de datos de su mundo (la celda donde se recogió la moneda). Las partes que solo pertenecen a estados pasados se recolectarán a tiempo.
Esto probablemente toma alguna consideración en el diseño de la estructura mundial del juego de datos de una manera apropiada. Lamentablemente, no soy un experto en estos asuntos. Definitivamente debe ser algo más que una matriz NxM que se usaría como una estructura de datos mutable. Probablemente debería consistir en piezas más pequeñas (corredores? Celdas individuales?) Que se apuntan entre sí, como lo hacen los nodos de los árboles.
fuente
La respuesta de 9000 es la mitad de la respuesta, las estructuras de datos persistentes le permiten reutilizar partes sin cambios.
Es posible que ya esté pensando "oye, ¿y si quiero cambiar la raíz del árbol?" tal como está con el ejemplo dado que ahora significa cambiar todos los nodos. Aquí es donde las cremalleras vienen al rescate. Permiten cambiar el elemento en un foco en O (1), y el foco se puede mover a cualquier parte de la estructura.
El otro punto con las cremalleras es que existe una Cremallera para casi cualquier tipo de datos que desee
fuente
Los programas de estilo funcional crean muchas oportunidades como esa para usar la concurrencia. Cada vez que transforma, filtra o agrega una colección, y todo es puro o inmutable, existe la oportunidad de acelerar la operación por concurrencia.
Por ejemplo, suponga que realiza decisiones de IA de forma independiente y sin ningún orden en particular. No se turnan, todos toman una decisión simultáneamente y luego el mundo avanza. El código podría verse así:
Tienes una función para calcular lo que hará un monstruo dado un estado mundial, y aplicarlo a cada monstruo como parte de la computación del próximo estado mundial. Esto es algo natural en un lenguaje funcional, y el compilador es libre de realizar el paso 'aplicarlo a cada monstruo' en paralelo.
En un lenguaje imperativo, es más probable que iteres sobre cada monstruo, aplicando sus efectos al mundo. Es más fácil hacerlo de esa manera, porque no desea lidiar con la clonación o el alias complicado. El compilador no puede realizar los cálculos de monstruos en paralelo en ese caso, porque las primeras decisiones de monstruos afectan las decisiones de monstruos posteriores.
fuente
Escuchar algunas charlas de Rich Hickey, esta en particular, alivió mi confusión. En uno indicó que está bien que los procesos concurrentes no tengan el estado más actual. Necesitaba escuchar eso. Lo que tenía problemas para digerir era que los programas en realidad estarían de acuerdo con basar las decisiones en instantáneas del mundo que desde entonces han sido reemplazadas por otras más nuevas. Seguía preguntándome cómo el FP concurrente resolvió el problema de basar las decisiones en el estado anterior.
En una aplicación bancaria nunca desearíamos basar una decisión en una instantánea del estado que desde entonces ha sido reemplazada por una nueva (se produjo un retiro).
Esa concurrencia es fácil porque el paradigma de FP evita el estado mutable es una afirmación técnica que no intenta decir nada sobre los méritos lógicos de basar las decisiones en un estado potencialmente antiguo. FP todavía en última instancia modela el cambio de estado. No hay forma de evitar esto.
fuente
Quería hablar sobre esta pregunta general como alguien que es un neófito funcional pero que ha estado a la altura de mis ojos en los efectos secundarios a lo largo de los años y me gustaría mitigarlos, por todo tipo de razones, incluso más fácil (o específicamente "más seguro, menos concurrencia "). Cuando miro a mis compañeros funcionales y lo que están haciendo, la hierba parece un poco más verde y huele mejor, al menos en este sentido.
Algoritmos seriales
Dicho esto, sobre su ejemplo específico, si su problema es de naturaleza serial y B no se puede ejecutar hasta que A esté terminado, conceptualmente no puede ejecutar A y B en paralelo sin importar qué. Debe encontrar una manera de romper la dependencia del orden, como en su respuesta, basándose en hacer movimientos paralelos utilizando el estado del juego anterior, o usar una estructura de datos que permita modificar partes de ella de forma independiente para eliminar la dependencia del orden como se propone en las otras respuestas o algo por el estilo. Pero definitivamente hay una serie de problemas de diseño conceptual como este en los que no se puede simplemente multiprocesar todo tan fácilmente porque las cosas son inmutables. Algunas cosas serán de naturaleza serial hasta que encuentres una forma inteligente de romper la dependencia del orden, si eso es posible.
Simultaneidad más fácil
Dicho esto, hay muchos casos en los que no somos capaces de poner en paralelo los programas que implican efectos secundarios en los lugares que podrían mejorar potencialmente el rendimiento significativamente simplemente debido a la posibilidad de que podría no ser seguro para subprocesos. Uno de los casos en los que la eliminación del estado mutable (o más específicamente, los efectos secundarios externos) ayuda mucho, según veo, es que convierte "puede o no ser seguro para subprocesos" en "definitivamente seguro para subprocesos" .
Para hacer esa afirmación un poco más concreta, considere que le doy una tarea para implementar una función de clasificación en C que acepta un comparador y lo usa para ordenar una matriz de elementos. Está destinado a ser bastante generalizado, pero supondré fácilmente que se utilizará contra entradas de tal escala (millones de elementos o más) que sin duda será beneficioso utilizar siempre una implementación multiproceso. ¿Puedes multiprocesar tu función de clasificación?
El problema es que no puede porque los comparadores a los que llama su función de clasificación puedencausar efectos secundarios a menos que sepa cómo se implementan (o al menos se documentan) para todos los casos posibles, lo cual es imposible sin desgeneralizar la función. Un comparador podría hacer algo desagradable como modificar una variable global dentro de una manera no atómica. Es posible que el 99.9999% de los comparadores no hagan esto, pero aún no podemos multiprocesar esta función generalizada simplemente debido al 0.00001% de los casos que pueden causar efectos secundarios. Como resultado, es posible que deba ofrecer una función de clasificación de subprocesos múltiples y de subprocesos múltiples y transferir la responsabilidad a los programadores que la utilizan para decidir cuál usar en función de la seguridad de los subprocesos. Y las personas aún pueden usar la versión de subproceso único y perder oportunidades de subprocesos múltiples porque también pueden no estar seguros de si el comparador es seguro para subprocesos,
Hay una gran cantidad de capacidad intelectual que puede estar involucrada en la racionalización de la seguridad de las cosas sin tirar cerraduras a todas partes que pueden desaparecer si solo tuviéramos garantías firmes de que las funciones no causarán efectos secundarios por ahora y el futuro. Y hay miedo: miedo práctico, porque cualquiera que haya tenido que depurar una condición de carrera demasiadas veces probablemente dudaría en hacer múltiples subprocesos cualquier cosa de la que no pueda estar 110% seguro de que es seguro para subprocesos y permanecerá como tal. Incluso para los más paranoicos (de los cuales probablemente estoy al menos en el límite), la función pura proporciona esa sensación de alivio y confianza que podemos llamar con seguridad en paralelo.
Y ese es uno de los casos principales donde lo veo tan beneficioso si puede obtener una garantía sólida de que tales funciones son seguras para subprocesos que obtiene con lenguajes funcionales puros. El otro es que los lenguajes funcionales a menudo promueven la creación de funciones libres de efectos secundarios en primer lugar. Por ejemplo, podrían proporcionarle estructuras de datos persistentes donde es razonablemente bastante eficiente ingresar una estructura de datos masiva y luego generar una nueva con solo una pequeña parte de ella modificada del original sin tocar el original. Aquellos que trabajan sin tales estructuras de datos pueden querer modificarlos directamente y perder algo de seguridad de subprocesos en el camino.
Efectos secundarios
Dicho esto, no estoy de acuerdo con una parte con el debido respeto a mis amigos funcionales (que creo que son súper geniales):
No es necesariamente la inmutabilidad lo que hace que la concurrencia sea tan práctica como la veo. Son funciones que evitan causar efectos secundarios. Si una función ingresa una matriz para ordenar, la copia y luego muta la copia para ordenar su contenido y genera la copia, sigue siendo tan segura como una que trabaja con algún tipo de matriz inmutable, incluso si está pasando la misma entrada matriz de múltiples hilos. Así que creo que todavía hay un lugar para los tipos mutables en la creación de código muy amigable con la concurrencia, por así decirlo, aunque hay muchos beneficios adicionales para los tipos inmutables, incluidas las estructuras de datos persistentes que utilizo no tanto por sus propiedades inmutables sino para elimine el gasto de tener que copiar todo en profundidad para crear funciones libres de efectos secundarios.
Y a menudo hay gastos generales para hacer que las funciones estén libres de efectos secundarios en forma de barajar y copiar algunos datos adicionales, tal vez un nivel adicional de indirección, y posiblemente algo de GC en partes de una estructura de datos persistente, pero miro a uno de mis amigos que tiene una máquina de 32 núcleos y creo que el intercambio probablemente valga la pena si podemos hacer más cosas con confianza en paralelo.
fuente