java - react - Beneficios no técnicos de tener un tipo de cuerda inmutable.
mutable wikipedia (8)
Me pregunto sobre los beneficios de tener el tipo de cadena inmutable desde el punto de vista de los programadores.
Los beneficios técnicos (en el lado del compilador / idioma) se pueden resumir principalmente en que es más fácil hacer optimizaciones si el tipo es inmutable. Lea here para una pregunta relacionada.
Además, en un tipo de cadena mutable, o bien ya tienes la seguridad de subprocesos incorporada (nuevamente, las optimizaciones son más difíciles de hacer) o tienes que hacerlo tú mismo. En cualquier caso, tendrá la opción de utilizar un tipo de cadena mutable con seguridad de subprocesos incorporada, por lo que no es realmente una ventaja de los tipos de cadena inmutables. (Una vez más, será más fácil realizar el manejo y las optimizaciones para garantizar la seguridad de los hilos en el tipo inmutable, pero ese no es el punto aquí).
Pero, ¿cuáles son los beneficios de los tipos de cadena inmutables en el uso? ¿Cuál es el punto de tener algunos tipos inmutables y otros no? Eso me parece muy inconsistente.
En C ++, si quiero que alguna cadena sea inmutable, la estoy pasando como referencia constante a una función ( const std::string&
). Si quiero tener una copia modificable de la cadena original, la estoy pasando como std::string
. Solo si quiero tenerlo mutable, lo paso como referencia ( std::string&
). Así que solo tengo la opción de lo que quiero hacer. Simplemente puedo hacer esto con todos los tipos posibles.
En Python o en Java, algunos tipos son inmutables (en su mayoría, todos los tipos y cadenas primitivos), otros no.
En lenguajes puramente funcionales como Haskell, todo es inmutable.
¿Hay alguna buena razón por la que tenga sentido tener esta inconsistencia? ¿O es simplemente por razones técnicas de bajo nivel?
¿Cuál es el punto de tener algunos tipos inmutables y otros no?
Sin algunos tipos mutables, tendría que dedicarse a la programación puramente funcional, un paradigma completamente diferente al OOP y los enfoques de procedimiento que son actualmente los más populares y, aunque son extremadamente poderosos, aparentemente muy desafiantes para muchos programadores. (lo que sucede cuando necesita efectos secundarios en un lenguaje en el que nada es mutable y en la programación del mundo real, por supuesto que inevitablemente lo hace, es parte del desafío: las Monads de Haskell son un enfoque muy elegante, por ejemplo, pero cuántos programadores, ¿saben que los entienden con toda confianza y pueden usarlos tan bien como las construcciones OOP típicas? -).
Si no comprende el enorme valor de tener múltiples paradigmas disponibles (tanto el FP como el que se basa fundamentalmente en datos mutables), recomiendo estudiar la obra maestra, conceptos, técnicas y modelos de programación de computadoras de Haridi y Van Roy, "un SICP para el siglo XXI ", como lo describí una vez ;-).
La mayoría de los programadores, ya estén familiarizados con Haridi y Van Roy o no, admitirán fácilmente que tener al menos algunos tipos de datos mutables es importante para ellos. A pesar de la frase que he citado anteriormente de su Q, que tiene un punto de vista completamente diferente, creo que también puede ser la raíz de su perplejidad: no "por qué algunos de cada uno", sino más bien "por qué algunos inmutables en absoluto".
El enfoque "completamente mutable" se obtuvo una vez (accidentalmente) en una implementación de Fortran. Si tuvieras, di,
SUBROUTINE ZAP(I)
I = 0
RETURN
a continuación, un fragmento de programa haciendo, por ejemplo,
PRINT 23
ZAP(23)
PRINT 23
imprimirá 23, luego 0; el número 23 se habrá mutado, por lo que todas las referencias a 23 en el resto del programa se referirán a 0. No es un error en el compilador, técnicamente: Fortran tenía reglas sutiles sobre lo que su programa Es y no se puede hacer al pasar constantes vs variables a procedimientos que se asignan a sus argumentos, y este fragmento de código viola esas reglas poco conocidas, no exigibles por el compilador, por lo que es un problema en el programa, no en el compilador. En la práctica, por supuesto, el número de errores causados de esta manera fue inaceptablemente alto, por lo que los compiladores típicos pronto cambiaron a un comportamiento menos destructivo en tales situaciones (colocando constantes en segmentos de solo lectura para obtener un error de tiempo de ejecución, si el SO lo admite; o , pasando una copia nueva de la constante en lugar de la constante en sí misma, a pesar de la sobrecarga, y así sucesivamente), aunque técnicamente eran errores de programa que permitían al compilador mostrar un comportamiento indefinido bastante "correctamente" ;-).
La alternativa que se aplica en algunos otros idiomas es agregar la complicación de múltiples formas de pasar parámetros, especialmente en C ++, que con valor por, valor por referencia, por referencia constante, por puntero, por puntero constante, ... y luego, por supuesto, verá programadores desconcertados por declaraciones como const foo* const bar
(donde la const
más a la derecha es básicamente irrelevante si bar
es un argumento de alguna función ... pero crucial si la bar
es una variable local ... ).
En realidad, Algol-68 probablemente fue más lejos en esta dirección (si puede tener un valor y una referencia, ¿por qué no una referencia a una referencia o una referencia a una referencia a una referencia? & C - Algol 68 no pone limitaciones a esto, y las reglas para definir lo que estaba ocurriendo es quizás la combinación más sutil y más difícil jamás encontrada en un lenguaje de programación "destinado a un uso real". La C temprana (que solo tenía un valor de by-point y un indicador explícito, sin const
, sin referencias, sin complicaciones) fue sin duda en parte una reacción a ella, al igual que el Pascal original. Pero const
pronto entró, y las complicaciones comenzaron a aumentar de nuevo.
Java y Python (entre otros lenguajes) cortan este matorral con un potente machete de simplicidad: todo el paso de argumentos y todas las asignaciones son "por referencia de objeto" (nunca se hace referencia a una variable u otra referencia, nunca a copias semánticamente implícitas, & c) . Definir (al menos) los números como semánticamente inmutables preserva la cordura de los programadores (así como este precioso aspecto de la simplicidad del lenguaje) al evitar "oopses" como los que se muestran en el código de Fortran.
Tratar las cadenas como primitivas como números es bastante consistente con el alto nivel semántico que pretenden los idiomas, porque en la vida real necesitamos cadenas que sean tan simples de usar como los números; las alternativas como definir cadenas como listas de caracteres (Haskell) o como matrices de caracteres (C) plantean desafíos tanto para el compilador (mantener un rendimiento eficiente bajo dicha semántica) como para el programador (ignorando esta estructuración arbitraria para permitir el uso de cadenas como simple primitivas, como a menudo requiere la programación de la vida real).
Python fue un poco más allá al agregar un simple contenedor inmutable ( tuple
) y vincular el hashing a la "inmutabilidad efectiva" (lo que evita ciertas sorpresas para el programador que se encuentran, por ejemplo, en Perl, con sus hashes que permiten cadenas mutables como claves) - ¿y por qué no? Una vez que tenga la inmutabilidad (un concepto precioso que evita que el programador tenga que aprender acerca de N diferentes semánticas para la asignación y el paso de argumentos, con N tiende a aumentar con el tiempo ;-), también podría obtener el kilometraje completo ;-) .
En un lenguaje con semántica de referencia para tipos definidos por el usuario, tener cadenas mutables sería un desastre, porque cada vez que asigne una variable de cadena, alias un objeto de cadena mutable y tendrá que hacer copias defensivas en todo el lugar. Es por eso que las cadenas son inmutables en Java y C #: si el objeto de la cadena es inmutable, no importa cuántas variables lo apunten.
Tenga en cuenta que en C ++, dos variables de cadena nunca comparten el estado (al menos de manera conceptual, técnicamente, puede haber una copia en escritura , pero eso se está desactualizando debido a las ineficiencias en los escenarios de subprocesos múltiples).
La principal ventaja para el programador es que con cadenas mutables, nunca debe preocuparse por quién podría alterar su cadena. Por lo tanto, nunca tiene que decidir conscientemente "¿Debo copiar esta cadena aquí?".
No estoy seguro de si consideraría esto como un beneficio de "nivel técnico bajo", pero el hecho de que la cadena inmutable esté implícitamente en hebras le ahorra mucho esfuerzo de codificación para la seguridad de las hebras.
Un poco de juguete ejemplo ...
Hilo A: verifique el usuario con el nombre de inicio de sesión. FOO tiene permiso para hacer algo, devolverlo
Subproceso B - Modificar la cadena de usuario al nombre de inicio de sesión BAR
Subproceso A: realice alguna operación con el nombre de inicio de sesión BAR debido a una verificación de permiso previa que pasa contra FOO.
El hecho de que la Cadena no pueda cambiar le ahorra el esfuerzo de protegerse contra esto.
No estoy seguro si esto califica como no técnico, sin embargo: si las cadenas son mutables, la mayoría de las colecciones (*) necesitan hacer copias privadas de sus claves de cadena.
De lo contrario, una clave "foo" cambiada externamente a "barra" daría lugar a que "barra" se asiente en las estructuras internas de la colección donde se espera "foo". De esta manera, la búsqueda "foo" encontraría "barra", que es un problema menor (no devolver nada, reindexar la clave ofensiva), pero la búsqueda "barra" no encontraría nada, lo cual es un problema mayor.
(*) Una colección tonta que haga un escaneo lineal de todas las claves en cada búsqueda no tendría que hacerlo, ya que naturalmente acomodaría cambios clave.
No hay una razón fundamental, fundamental para no tener cadenas mutables. La mejor explicación que he encontrado para su inmutabilidad es que promueve una forma de programación más funcional y con menos efectos secundarios. Esto termina siendo más limpio, más elegante y más pitón.
Semánticamente, deberían ser inmutables, ¿no? La cadena "hello"
siempre debe representar "hello"
. ¡No puedes cambiarlo más de lo que puedes cambiar el número tres!
Si las cadenas son mutables, entonces muchos consumidores de una cadena tendrán que hacer copias de la misma. Si las cadenas son inmutables, esto es mucho menos importante (a menos que la inmutabilidad sea impuesta por los interbloqueos de hardware, puede que no sea una mala idea para algunos consumidores conscientes de la seguridad de una cadena hacer sus propias copias en caso de que las cadenas que se les proporcionen no lo sean ». Tan inmutable como deberían ser).
La clase StringBuilder es bastante buena, aunque creo que sería mejor si tuviera una propiedad de "Valor" (la lectura sería equivalente a ToString, pero se mostraría en los inspectores de objetos; la escritura permitiría la configuración directa de todo el contenido) y una conversión de ampliación por defecto a una cadena. Hubiera sido bueno en teoría tener el tipo MutableString descendido de un ancestro común con String, por lo que se podría pasar una cadena mutable a una función que no le importaba si una cadena era mutable, aunque sospecho que las optimizaciones que se basan en el hecho Que las cadenas tengan una cierta implementación fija, hubieran sido menos efectivas.
Si quieres una consistencia total, solo puedes hacer que todo sea inmutable , porque Bools o Ints mutables simplemente no tendrían ningún sentido. Algunos lenguajes funcionales hacen eso de hecho.
La filosofía de Python es "Lo simple es mejor que lo complejo". En C, debes ser consciente de que las cadenas pueden cambiar y pensar cómo te puede afectar eso. Python asume que el caso de uso predeterminado para cadenas es "poner texto juntos", no hay absolutamente nada que necesites saber sobre cadenas para hacer eso. Pero si desea que sus cadenas cambien, solo tiene que usar un tipo más apropiado (por ejemplo, listas, StringIO, plantillas, etc.).