En general, los tipos inmutables creados en lenguajes que no giran en torno a la inmutabilidad tenderán a costar más tiempo al desarrollador para crearlos y potencialmente a usarlos si requieren algún tipo de objeto "generador" para expresar los cambios deseados (esto no significa que el trabajo será más, pero hay un costo por adelantado en estos casos). Además, independientemente de si el lenguaje hace que sea realmente fácil crear tipos inmutables o no, siempre requerirá algo de procesamiento y sobrecarga de memoria para los tipos de datos no triviales.
Hacer que las funciones carezcan de efectos secundarios
Si está trabajando en idiomas que no giran en torno a la inmutabilidad, entonces creo que el enfoque pragmático no es tratar de hacer que cada tipo de datos sea inmutable. Una mentalidad potencialmente mucho más productiva que le brinda muchos de los mismos beneficios es enfocarse en maximizar la cantidad de funciones en su sistema que causan cero efectos secundarios .
Como ejemplo simple, si tiene una función que causa un efecto secundario como este:
// Make 'x' the absolute value of itself.
void make_abs(int& x);
Entonces no necesitamos un tipo de datos entero inmutable que prohíba a los operadores como la asignación posterior a la inicialización para que esa función evite los efectos secundarios. Simplemente podemos hacer esto:
// Returns the absolute value of 'x'.
int abs(int x);
Ahora la función no se mete x
ni nada fuera de su alcance, y en este caso trivial, incluso podríamos haber afeitado algunos ciclos evitando cualquier sobrecarga asociada con la indirección / alias. Por lo menos, la segunda versión no debería ser más costosa desde el punto de vista computacional que la primera.
Cosas que son caras de copiar en su totalidad
Por supuesto, la mayoría de los casos no son tan triviales si queremos evitar que una función cause efectos secundarios. Un caso de uso complejo en el mundo real podría ser más parecido a esto:
// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);
En ese momento, la malla puede requerir un par de cientos de megabytes de memoria con más de cien mil polígonos, incluso más vértices y bordes, múltiples mapas de textura, objetivos de transformación, etc. Sería muy costoso copiar toda la malla solo para hacer esto transform
Función libre de efectos secundarios, así:
// Returns a new version of the mesh whose vertices been
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);
Y es en estos casos donde copiar algo en su totalidad normalmente sería una sobrecarga épica en la que he encontrado útil convertirme Mesh
en una estructura de datos persistente y en un tipo inmutable con el "generador" analógico para crear versiones modificadas de la misma. puede simplemente copiar superficialmente y contar piezas que no son únicas. Todo se centra en poder escribir funciones de malla que no tengan efectos secundarios.
Estructuras de datos persistentes
Y en estos casos donde copiar todo es tan increíblemente costoso, encontré el esfuerzo de diseñar un sistema inmutable Mesh
que realmente valga la pena a pesar de que tenía un costo inicial ligeramente elevado, ya que no solo simplificaba la seguridad del hilo. También simplificó la edición no destructiva (permitiendo al usuario aplicar capas a las operaciones de malla sin modificar su copia original), deshacer sistemas (ahora el sistema de deshacer puede almacenar una copia inmutable de la malla antes de los cambios realizados por una operación sin destruir memoria use), y excepción-seguridad (ahora si ocurre una excepción en la función anterior, la función no tiene que retroceder y deshacer todos sus efectos secundarios ya que no causó ninguno para empezar).
Puedo decir con confianza en estos casos que el tiempo requerido para hacer que estas estructuras de datos pesadas sean inmutables ahorró más tiempo del que costó, ya que comparé los costos de mantenimiento de estos nuevos diseños con los anteriores que giraban en torno a la mutabilidad y las funciones que causan efectos secundarios, y los diseños mutables anteriores costaban mucho más tiempo y eran mucho más propensos a errores humanos, especialmente en áreas que son realmente tentadores para los desarrolladores que descuidan durante el tiempo de crisis, como la seguridad de excepción.
Por lo tanto, creo que los tipos de datos inmutables realmente dan resultado en estos casos, pero no todo tiene que hacerse inmutable para que la mayoría de las funciones de su sistema estén libres de efectos secundarios. Muchas cosas son lo suficientemente baratas como para copiarlas en su totalidad. Además, muchas aplicaciones del mundo real necesitarán causar algunos efectos secundarios aquí y allá (al menos como guardar un archivo), pero generalmente hay muchas más funciones que podrían estar desprovistas de efectos secundarios.
El punto de tener algunos tipos de datos inmutables para mí es asegurarnos de que podamos escribir el número máximo de funciones para estar libres de efectos secundarios sin incurrir en una sobrecarga épica en forma de copia profunda de estructuras de datos masivas de izquierda a derecha en su totalidad cuando solo pequeñas porciones de ellos necesitan ser modificados. Tener estructuras de datos persistentes en esos casos termina convirtiéndose en un detalle de optimización que nos permite escribir nuestras funciones para estar libres de efectos secundarios sin pagar un costo épico por hacerlo.
Sobrecarga inmutable
Ahora conceptualmente, las versiones mutables siempre tendrán una ventaja en eficiencia. Siempre existe esa sobrecarga computacional asociada con estructuras de datos inmutables. Pero me pareció un intercambio digno en los casos que describí anteriormente, y puede concentrarse en hacer que los gastos generales sean lo suficientemente mínimos por naturaleza. Prefiero ese tipo de enfoque donde la corrección se vuelve fácil y la optimización se vuelve más difícil en lugar de que la optimización sea más fácil, pero la corrección se vuelve más difícil. No es tan desmoralizador tener un código que funcione perfectamente correctamente y necesite más ajustes sobre el código que no funciona correctamente en primer lugar, sin importar cuán rápido logre sus resultados incorrectos.