Hay algunos buenos ejemplos aquí, pero quería saltar con algunos personales donde la inmutabilidad ayudó mucho. En mi caso, comencé a diseñar una estructura de datos simultánea inmutable principalmente con la esperanza de poder ejecutar el código con seguridad en paralelo con lecturas y escrituras superpuestas y no tener que preocuparme por las condiciones de la carrera. Hubo una charla que John Carmack me dio que me inspiró a hacerlo cuando habló de tal idea. Es una estructura bastante básica y bastante trivial para implementar de esta manera:
Por supuesto, con algunas campanas y silbatos más como poder eliminar elementos en tiempo constante y dejar agujeros recuperables y hacer que los bloques se vuelvan a deshacer si se vuelven vacíos y potencialmente liberados para una instancia inmutable dada. Pero, básicamente, para modificar la estructura, modifica una versión "transitoria" y confirma atómicamente los cambios que realizó para obtener una nueva copia inmutable que no toque la anterior, y la nueva versión solo crea nuevas copias de los bloques que deben hacerse únicos mientras se copian poco a poco y se cuentan las referencias.
Sin embargo, no me pareció queútil para propósitos de subprocesos múltiples. Después de todo, todavía existe el problema conceptual en el que, por ejemplo, un sistema de física aplica la física simultáneamente mientras un jugador está tratando de mover elementos en un mundo. ¿Con qué copia inmutable de los datos transformados vas, la que el jugador transformó o la que transformó el sistema físico? Por lo tanto, realmente no he encontrado una solución agradable y simple para este problema conceptual básico, excepto tener estructuras de datos mutables que simplemente se bloquean de una manera más inteligente y desalientan las lecturas y escrituras superpuestas en las mismas secciones del búfer para evitar detener los hilos. Eso es algo que John Carmack parece haber descubierto cómo resolver en sus juegos; al menos habla de eso como si casi pudiera ver una solución sin abrir un auto de gusanos. No he llegado tan lejos como él en ese sentido. Todo lo que puedo ver son interminables preguntas de diseño si solo intento paralelizar todo alrededor de los inmutables. Desearía poder pasar un día hurgando en su cerebro ya que la mayoría de mis esfuerzos comenzaron con esas ideas que él arrojó.
Sin embargo, encontré un enorme valor de esta estructura de datos inmutable en otras áreas. Incluso lo uso ahora para almacenar imágenes, lo cual es realmente extraño y hace que el acceso aleatorio requiera algunas instrucciones más (desplazamiento a la derecha y un poco a lo and
largo junto con una capa de dirección indirecta del puntero), pero cubriré los beneficios a continuación.
Deshacer sistema
Uno de los lugares más inmediatos que encontré para beneficiarme de esto fue el sistema de deshacer. El código del sistema de deshacer solía ser una de las cosas más propensas a errores en mi área (industria visual FX), y no solo en los productos en los que trabajé sino en los productos de la competencia (sus sistemas de deshacer también eran inestables) porque había muchas diferencias tipos de datos para preocuparse por deshacer y rehacer correctamente (sistema de propiedad, cambios de datos de malla, cambios de sombreador que no se basaron en propiedades como el intercambio de uno con el otro, cambios en la jerarquía de la escena como cambiar el padre de un hijo, cambios de imagen / textura, etc. etc. etc.).
Por lo tanto, la cantidad de código de deshacer requerida era enorme, a menudo rivalizando con la cantidad de código que implementaba el sistema para el cual el sistema de deshacer tenía que registrar los cambios de estado. Al apoyarme en esta estructura de datos, pude hacer que el sistema de deshacer se redujera a esto:
on user operation:
copy entire application state to undo entry
perform operation
on undo/redo:
swap application state with undo entry
Normalmente, el código anterior sería enormemente ineficiente cuando los datos de su escena abarcan gigabytes para copiarlos en su totalidad. Pero esta estructura de datos solo copia cosas poco profundas que no se modificaron, y en realidad lo hizo lo suficientemente barato como para almacenar una copia inmutable de todo el estado de la aplicación. Así que ahora puedo implementar sistemas de deshacer tan fácilmente como el código anterior y solo enfocarme en usar esta estructura de datos inmutable para hacer que copiar partes no modificadas del estado de la aplicación sea más barato y más barato. Desde que comencé a usar esta estructura de datos, todos mis proyectos personales tienen sistemas de deshacer simplemente usando este patrón simple.
Ahora todavía hay algo de gastos generales aquí. La última vez que medí fue alrededor de 10 kilobytes solo para copiar superficialmente el estado completo de la aplicación sin hacer ningún cambio (esto es independiente de la complejidad de la escena ya que la escena está organizada en una jerarquía, por lo que si nada debajo de la raíz cambia, solo la raíz se copia poco profundo sin tener que descender hacia los niños). Eso está lejos de 0 bytes, ya que sería necesario para un sistema de deshacer que solo almacena deltas. Pero a 10 kilobytes de gastos generales de deshacer por operación, eso sigue siendo solo un megabyte por cada 100 operaciones de usuario. Además, aún podría aplastarlo aún más en el futuro si fuera necesario.
Excepción-Seguridad
La seguridad de excepción con una aplicación compleja no es un asunto trivial. Sin embargo, cuando el estado de su aplicación es inmutable y solo está utilizando objetos transitorios para intentar realizar transacciones de cambio atómico, entonces es inherentemente seguro para excepciones, ya que si se arroja alguna parte del código, el transitorio se descarta antes de dar una nueva copia inmutable . Así que eso trivializa una de las cosas más difíciles que siempre he encontrado en un código base de C ++ complejo.
Demasiadas personas a menudo solo usan recursos conformes con RAII en C ++ y piensan que es suficiente para estar a salvo de excepciones. A menudo no lo es, ya que una función generalmente puede causar efectos secundarios a estados más allá de los locales a su alcance. Por lo general, debe comenzar a tratar con protectores de alcance y una lógica de reversión sofisticada en esos casos. Esta estructura de datos lo hizo así que a menudo no necesito molestarme con eso ya que las funciones no están causando efectos secundarios. Están devolviendo copias inmutables transformadas del estado de la aplicación en lugar de transformar el estado de la aplicación.
Edición no destructiva
La edición no destructiva es básicamente operaciones de capas / apilamiento / conexión juntas sin tocar los datos del usuario original (solo datos de entrada y datos de salida sin tocar la entrada). Por lo general, es trivial implementarlo con una aplicación de imagen simple como Photoshop y es posible que no se beneficie tanto de esta estructura de datos, ya que muchas operaciones pueden simplemente querer transformar cada píxel de toda la imagen.
Sin embargo, con la edición de malla no destructiva, por ejemplo, muchas operaciones a menudo quieren transformar solo una parte de la malla. Una operación puede querer mover algunos vértices aquí. Otro podría simplemente querer subdividir algunos polígonos allí. Aquí, la estructura de datos inmutable ayuda mucho a evitar la necesidad de tener que hacer una copia completa de la malla completa solo para devolver una nueva versión de la malla con una pequeña parte de ella modificada.
Minimizando los efectos secundarios
Con estas estructuras en la mano, también facilita la escritura de funciones que minimizan los efectos secundarios sin incurrir en una gran penalización de rendimiento. Me he encontrado escribiendo más y más funciones que solo devuelven estructuras de datos inmutables por valor en estos días sin incurrir en efectos secundarios, incluso cuando parece un poco inútil.
Por ejemplo, típicamente la tentación de transformar un montón de posiciones podría ser aceptar una matriz y una lista de objetos y transformarlos de manera mutable. En estos días me encuentro devolviendo una nueva lista de objetos.
Cuando tiene más funciones como esta en su sistema que no causan efectos secundarios, definitivamente hace que sea más fácil razonar sobre su corrección y probar su corrección.
Los beneficios de las copias baratas
De todos modos, estas son las áreas donde encontré el mayor uso de estructuras de datos inmutables (o estructuras de datos persistentes). También me puse un poco entusiasta al principio e hice un árbol inmutable y una lista enlazada inmutable y una tabla hash inmutable, pero con el tiempo rara vez encontré tanto uso para esos. Principalmente encontré el mayor uso del grueso contenedor inmutable tipo matriz en el diagrama anterior.
También todavía tengo mucho código trabajando con mutables (considero que es una necesidad práctica al menos para el código de bajo nivel), pero el estado principal de la aplicación es una jerarquía inmutable, que se desglosa de una escena inmutable a componentes inmutables dentro de ella. Algunos de los componentes más baratos todavía se copian en su totalidad, pero los más caros, como las mallas y las imágenes, usan la estructura inmutable para permitir esas copias baratas parciales de solo las partes que necesitan ser transformadas.
ConcurrentModificationException
que generalmente es causado por el mismo hilo que muta la colección en el mismo hilo, en el cuerpo de unforeach
bucle sobre la misma colección.