Una función que acepta algún valor y devuelve algún otro valor, y no altera nada fuera de la función, no tiene efectos secundarios y, por lo tanto, es segura para subprocesos. Si desea considerar cosas como la forma en que se ejecuta la función afecta el consumo de energía, ese es un problema diferente.
Supongo que se refiere a una máquina completa de Turing que está ejecutando algún tipo de lenguaje de programación bien definido, donde los detalles de implementación son irrelevantes. En otras palabras, no debería importar lo que esté haciendo la pila, si la función que estoy escribiendo en mi lenguaje de programación de elección puede garantizar la inmutabilidad dentro de los límites del lenguaje. No pienso en la pila cuando estoy programando en un lenguaje de alto nivel, ni debería tener que hacerlo.
Para ilustrar cómo funciona esto, voy a ofrecer algunos ejemplos simples en C #. Para que estos ejemplos sean ciertos, tenemos que hacer un par de suposiciones. Primero, que el compilador sigue la especificación de C # sin error, y segundo, que produce los programas correctos.
Digamos que quiero una función simple que acepte una colección de cadenas y devuelva una cadena que sea una concatenación de todas las cadenas de la colección separadas por comas. Una implementación simple e ingenua en C # podría verse así:
public string ConcatenateWithCommas(ImmutableList<string> list)
{
string result = string.Empty;
bool isFirst = false;
foreach (string s in list)
{
if (isFirst)
result += s;
else
result += ", " + s;
}
return result;
}
Este ejemplo es inmutable, prima facie. ¿Cómo sé eso? Porque el string
objeto es inmutable. Sin embargo, la implementación no es ideal. Debido a que result
es inmutable, debe crearse un nuevo objeto de cadena cada vez a través del bucle, reemplazando el objeto original al que result
apunta. Esto puede afectar negativamente la velocidad y ejercer presión sobre el recolector de basura, ya que tiene que limpiar todas esas cadenas adicionales.
Ahora, digamos que hago esto:
public string ConcatenateWithCommas(ImmutableList<string> list)
{
var result = new StringBuilder();
bool isFirst = false;
foreach (string s in list)
{
if (isFirst)
result.Append(s);
else
result.Append(", " + s);
}
return result.ToString();
}
Tenga en cuenta que he reemplazado string
result
con un objeto mutable, StringBuilder
. Esto es mucho más rápido que el primer ejemplo, porque no se crea una nueva cadena cada vez a través del bucle. En cambio, el objeto StringBuilder simplemente agrega los caracteres de cada cadena a una colección de caracteres, y genera todo al final.
¿Es esta función inmutable, aunque StringBuilder es mutable?
Sí lo es. ¿Por qué? Como cada vez que se llama a esta función, se crea un nuevo StringBuilder, solo para esa llamada. Así que ahora tenemos una función pura que es segura para subprocesos, pero contiene componentes mutables.
Pero, ¿y si hiciera esto?
public class Concatenate
{
private StringBuilder result = new StringBuilder();
bool isFirst = false;
public string ConcatenateWithCommas(ImmutableList<string> list)
{
foreach (string s in list)
{
if (isFirst)
result.Append(s);
else
result.Append(", " + s);
}
return result.ToString();
}
}
¿Es este método seguro para subprocesos? No lo es. ¿Por qué? Porque la clase ahora tiene un estado del que depende mi método. Ahora está presente una condición de carrera en el método: un subproceso puede modificarse IsFirst
, pero otro subproceso puede realizar el primero Append()
, en cuyo caso ahora tengo una coma al comienzo de mi cadena que no se supone que esté allí.
¿Por qué podría querer hacerlo así? Bueno, es posible que desee que los hilos acumulen las cadenas en mi result
sin importar el orden, o en el orden en que entran los hilos. Quizás sea un registrador, ¿quién sabe?
De todos modos, para solucionarlo, puse una lock
declaración alrededor de las entrañas del método.
public class Concatenate
{
private StringBuilder result = new StringBuilder();
bool isFirst = false;
private static object locker = new object();
public string AppendWithCommas(ImmutableList<string> list)
{
lock (locker)
{
foreach (string s in list)
{
if (isFirst)
result.Append(s);
else
result.Append(", " + s);
}
return result.ToString();
}
}
}
Ahora es seguro para subprocesos nuevamente.
La única forma en que mis métodos inmutables podrían no ser seguros para subprocesos es si el método de alguna manera pierde parte de su implementación. ¿Podría pasar esto? No si el compilador es correcto y el programa es correcto. ¿Alguna vez necesitaré bloqueos en tales métodos? No.
Para ver un ejemplo de cómo se podría filtrar la implementación en un escenario de concurrencia, consulte aquí .
but everything has a side effect
- Uh, no, no lo hace. Una función que acepta algún valor y devuelve algún otro valor, y no altera nada fuera de la función, no tiene efectos secundarios y, por lo tanto, es segura para subprocesos. No importa que la computadora use electricidad. También podemos hablar sobre los rayos cósmicos que golpean las células de memoria, si lo desea, pero mantengamos el argumento práctico. Si desea considerar cosas como la forma en que se ejecuta la función afecta el consumo de energía, ese es un problema diferente que la programación segura para subprocesos.