¿Qué puede salir mal en el contexto de la programación funcional si mi objeto es mutable?

9

Puedo ver los beneficios de los objetos mutables frente a los inmutables, como los objetos inmutables que eliminan muchos problemas difíciles de solucionar en la programación de subprocesos múltiples debido al estado compartido y de escritura. Por el contrario, los objetos mutables ayudan a tratar con la identidad del objeto en lugar de crear una copia nueva cada vez y, por lo tanto, también mejoran el rendimiento y el uso de la memoria, especialmente para objetos más grandes.

Una cosa que estoy tratando de entender es qué puede salir mal al tener objetos mutables en el contexto de la programación funcional. Como uno de los puntos que me dijeron es que el resultado de llamar a funciones en un orden diferente no es determinista.

Estoy buscando un ejemplo concreto real en el que sea muy evidente lo que puede salir mal usando un objeto mutable en la programación de funciones. Básicamente, si es malo, es malo independientemente de OO o paradigma de programación funcional, ¿verdad?

Creo que debajo de mi propia declaración responde esta pregunta. Pero aún así necesito algún ejemplo para poder sentirlo más naturalmente.

OO ayuda a gestionar la dependencia y a escribir programas más fáciles y fáciles de mantener con la ayuda de herramientas como encapsulación, polimorfismo, etc.

La programación funcional también tiene el mismo motivo de promover el código mantenible, pero mediante el uso de un estilo que elimina la necesidad de utilizar herramientas y técnicas de OO, una de las cuales creo que es minimizar los efectos secundarios, la función pura, etc.

rahulaga_dev
fuente
1
@Ruben, diría que la mayoría de los lenguajes funcionales permiten variables mutables, pero hacen que sea diferente usarlos, por ejemplo, las variables mutables tienen un tipo diferente
jk.
1
Creo que puede haber mezclado inmutable y mutable en su primer párrafo?
jk.
1
@jk., ciertamente lo hizo. Editado para corregir eso.
David Arno
66
@Ruben La programación funcional es un paradigma. Como tal, no requiere un lenguaje de programación funcional. Y algunos lenguajes fp como F # tienen esta característica .
Christophe
1
@Ruben no específicamente estaba pensando en Mvars en haskell hackage.haskell.org/package/base-4.9.1.0/docs/… diferentes idiomas tienen diferentes soluciones, por supuesto, o IORefs hackage.haskell.org/package/base-4.11.1.0 /docs/Data-IORef.html aunque, por supuesto, usaría ambos desde mónadas
jk.

Respuestas:

7

Creo que la importancia se demuestra mejor si se compara con un enfoque OO

por ejemplo, digamos que tenemos un objeto

Order
{
    string Status {get;set;}
    Purchase()
    {
        this.Status = "Purchased";
    }
}

En el paradigma OO, el método se adjunta a los datos, y tiene sentido que esos datos sean mutados por el método.

var order = new Order();
order.Purchase();
Console.WriteLine(order.Status); // "Purchased"

En el paradigma funcional definimos un resultado en términos de la función. un pedido comprado ES el resultado de la función de compra aplicada a un pedido. Esto implica algunas cosas de las que debemos estar seguros

var order = new Order(); //this is a 'new order'
var purchasedOrder = purchase(order); // this is a 'purchased order'
Console.WriteLine(order.Status); // "New" order is still a 'new order'

¿Esperarías order.Status == "Comprado"?

También implica que nuestras funciones son idempotentes. es decir. ejecutarlos dos veces debería producir el mismo resultado cada vez.

var order = new Order(); //new order
var purchasedOrder = purchase(order); //purchased order
var purchasedOrder2 = purchase(order); //another purchased order
var purchasedOrder = purchase(purchasedOrder); //error! cant purchase an order twice

Si la orden de compra fue modificada, la orden de compra2 fallará.

Al definir las cosas como resultados de funciones, nos permite usar esos resultados sin calcularlos realmente. Que en términos de programación es ejecución diferida.

Esto puede ser útil en sí mismo, pero una vez que no estamos seguros de cuándo sucederá realmente una función Y estamos bien al respecto, podemos aprovechar el procesamiento paralelo mucho más de lo que podemos en un paradigma OO.

Sabemos que ejecutar una función no afectará los resultados de otra función; para que podamos dejar que la computadora los ejecute en el orden que elija, utilizando tantos hilos como desee.

Si una función muta su entrada, debemos ser mucho más cuidadosos con esas cosas.

Ewan
fuente
Gracias !! muy útil. Así se vería una nueva implementación de compra Order Purchase() { return new Order(Status = "Purchased") } para que el estado sea un campo de solo lectura. ? Nuevamente, ¿por qué esta práctica es más relevante en el contexto del paradigma de programación de funciones? Los beneficios que mencionó también se pueden ver en la programación OO, ¿verdad?
rahulaga_dev
en OO esperarías que object.Purchase () modifique el objeto. Podría hacerlo inmutable, pero ¿por qué no pasar a un paradigma funcional completo
Ewan
Creo que tengo que visualizar el problema porque soy un desarrollador puro de C # que está orientado a objetos por naturaleza. Entonces, lo que dices en un lenguaje que abarque la programación funcional no requerirá que la función 'Comprar ()' devuelva el pedido comprado para adjuntarlo a cualquier clase u objeto, ¿verdad?
rahulaga_dev
3
puede escribir c # funcional, cambiar su objeto a una estructura, hacerlo inmutable y escribir una compra Func <Order, Order>
Ewan
12

La clave para comprender por qué los objetos inmutables son beneficiosos no radica realmente en tratar de encontrar ejemplos concretos en el código funcional. Dado que la mayoría del código funcional está escrito usando lenguajes funcionales, y la mayoría de los lenguajes funcionales son inmutables por defecto, la naturaleza misma del paradigma está diseñada para evitar que suceda lo que está buscando.

La pregunta clave es, ¿cuál es el beneficio de la inmutabilidad? La respuesta es que evita la complejidad. Digamos que tenemos dos variables, xy y. Ambos comienzan con el valor de 1. yaunque se duplica cada 13 segundos. ¿Cuál será el valor de cada uno de ellos dentro de 20 días? xserá 1. Eso es fácil. Sin embargo, tomaría un esfuerzo resolverlo, yya que es mucho más complejo. ¿A qué hora del día en 20 días? ¿Tengo que tener en cuenta el horario de verano? La complejidad de yversus xes mucho más.

Y esto también ocurre en código real. Cada vez que agrega un valor mutante a la mezcla, ese se convierte en otro valor complejo para que lo tenga en su cabeza y calcule en su cabeza, o en papel, cuando intente escribir, leer o depurar el código. Cuanta más complejidad, mayores serán las posibilidades de que cometas un error e introduzcas un error. El código es difícil de escribir; difícil de leer; difícil de depurar: el código es difícil de corregir.

Sin embargo, la mutabilidad no es mala . Un programa con cero mutabilidad no puede tener resultados, lo cual es bastante inútil. Incluso si la mutabilidad es escribir un resultado en la pantalla, el disco o lo que sea, debe estar allí. Lo que es malo es una complejidad innecesaria. Una de las formas más simples de reducir la complejidad es hacer que las cosas sean inmutables por defecto y solo hacerlas mutables cuando sea necesario, debido a razones de rendimiento o funcionales.

David Arno
fuente
44
"Una de las formas más simples de reducir la complejidad es hacer que las cosas sean inmutables por defecto y solo hacerlas mutables cuando sea necesario": Resumen muy agradable y conciso.
Giorgio
2
@DavidArno La complejidad que describe hace que el código sea difícil de razonar. También mencionó esto cuando dijo "El código es difícil de escribir; difícil de leer; difícil de depurar; ...". Me gustan los objetos inmutables porque hacen que el código sea mucho más fácil de razonar, no solo por mí mismo, sino también por observadores que observan sin conocer todo el proyecto.
desmontar-número-5
1
@RahulAgarwal, " Pero por qué este problema se vuelve más prominente en el contexto de la programación funcional ". No lo hace. Creo que quizás estoy confundido por lo que está preguntando, ya que el problema es mucho menos prominente en FP ya que FP fomenta la inmutabilidad evitando así el problema.
David Arno
1
@djechlin, " ¿Cómo puede ser más fácil analizar su ejemplo de 13 segundos con código inmutable? " No puede: ytiene que mutar; Eso es un requisito. A veces tenemos que tener un código complejo para cumplir con los requisitos complejos. El punto que estaba tratando de aclarar es que se debe evitar la complejidad innecesaria . Los valores de mutación son intrínsecamente más complejos que los fijos, por lo que, para evitar una complejidad innecesaria, solo mute los valores cuando sea necesario.
David Arno
3
La mutabilidad crea una crisis de identidad. Su variable ya no tiene una identidad única. En cambio, su identidad ahora depende del tiempo. Entonces, simbólicamente, en lugar de una sola x, ahora tenemos una familia x_t. Cualquier código que use esa variable ahora también tendrá que preocuparse por el tiempo, causando una complejidad adicional mencionada en la respuesta.
Alex Vong
8

¿Qué puede salir mal en el contexto de la programación funcional?

Las mismas cosas que pueden salir mal en la programación no funcional: puede obtener efectos secundarios no deseados e inesperados , que es una causa bien conocida de errores desde la invención de los lenguajes de programación.

En mi humilde opinión, la única diferencia real en esto entre la programación funcional y no funcional es que, en el código no funcional, generalmente esperará efectos secundarios, en la programación funcional, no lo hará.

Básicamente, si es malo, es malo independientemente de OO o paradigma de programación funcional, ¿verdad?

Claro, los efectos secundarios no deseados son una categoría de errores, independientemente del paradigma. Lo contrario también es cierto: los efectos secundarios utilizados deliberadamente pueden ayudar a lidiar con los problemas de rendimiento y, por lo general, son necesarios para la mayoría de los programas del mundo real cuando se trata de E / S y de sistemas externos, también independientemente del paradigma.

Doc Brown
fuente
4

Acabo de responder una pregunta de StackOverflow que ilustra su pregunta bastante bien. El principal problema con las estructuras de datos mutables es que su identidad solo es válida en un instante exacto en el tiempo, por lo que las personas tienden a meter todo lo que pueden en el pequeño punto del código donde saben que la identidad es constante. En este ejemplo en particular, está registrando mucho dentro de un bucle for:

for (elem <- rows map (row => s3 map row)) {
  val elem_str = elem.map(_.toString)

  logger.info("verifying the S3 bucket passed from the ctrl table for each App")
  logger.info(s"Checking on App Code: ${elem head}")

  listS3Buckets(elem_str(1), elem_str(2)) match {

    case Some(allBktsInfo) =>
      logger.info(s"App: ${elem_str head} provided the bucket name as: ${elem_str(3)}")
      if (allBktsInfo.exists(x => x.getName == elem_str(3))) {
        logger.info(s"Provided S3 bucket: ${elem_str(3)} exists")
        println(s"s3 ${elem_str(3)} bucket exists")
      } else {
        logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
        logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
        excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
        println(s"s3 bucket ${elem_str(3)} doesn't exists")
    }

    case None =>
      logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
      logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
      excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
}

Cuando está acostumbrado a la inmutabilidad, no hay temor de que cambie la estructura de datos si espera demasiado, por lo que puede realizar tareas que están lógicamente separadas en su tiempo libre, de una manera mucho más desacoplada:

val (exists, missing) = rows partition bucketExists
missing foreach {row =>
  logger.info(s"WARNING: Provided S3 bucket ${row("s3_primary_bkt_name")} doesn't exist")
  logger.info(s"WARNING: Dropping the App: ${row("app")} from backup schedule")
}
Karl Bielefeldt
fuente
3

La ventaja de usar objetos inmutables es que si uno recibe una referencia a un objeto que tendrá cierta propiedad cuando el receptor lo examine, y necesita darle a otro código una referencia a un objeto con esa misma propiedad, simplemente puede pasar junto con la referencia al objeto sin tener en cuenta quién más podría haber recibido la referencia o qué podrían hacerle al objeto [ya que no hay nada que nadie más pueda hacerle al objeto], o cuándo el receptor podría examinar el objeto [ya que todo las propiedades serán las mismas independientemente de cuándo se examinen].

Por el contrario, el código que necesita dar a alguien una referencia a un objeto mutable que tendrá una cierta propiedad cuando el receptor lo examine (suponiendo que el receptor en sí no lo cambie) tampoco necesita saber que nada más que el receptor cambiará alguna vez esa propiedad, o bien saber cuándo el receptor accederá a esa propiedad, y saber que nada va a cambiar esa propiedad hasta la última vez que el receptor la examinará.

Creo que es más útil, para la programación en general (no solo la programación funcional) pensar que los objetos inmutables se dividen en tres categorías:

  1. Los objetos que no pueden no permitirán que nada los cambie, incluso con una referencia. Tales objetos, y referencias a ellos, se comportan como valores y pueden compartirse libremente.

  2. Objetos que permitirían ser cambiados por un código que tiene referencias a ellos, pero cuyas referencias nunca estarán expuestas a ningún código que realmente los cambie. Estos objetos encapsulan valores, pero solo se pueden compartir con código en el que se puede confiar para que no los cambie ni los exponga al código que podría hacerlo.

  3. Objetos que serán cambiados. Estos objetos se ven mejor como contenedores , y las referencias a ellos como identificadores .

A menudo, un patrón útil es hacer que un objeto cree un contenedor, lo llene usando un código en el que se pueda confiar para que no mantenga una referencia después, y luego tenga las únicas referencias que existirán en cualquier parte del universo que estén en un código que nunca modifique objeto una vez que está poblado. Si bien el contenedor puede ser de tipo mutable, se puede razonar sobre (*) como si fuera inmutable, ya que de hecho nada lo mutará. Si todas las referencias al contenedor se mantienen en tipos de envoltorios inmutables que nunca alterarán su contenido, dichos envoltorios se pueden pasar de forma segura como si los datos dentro de ellos estuvieran en objetos inmutables, ya que las referencias a los envoltorios se pueden compartir y examinar libremente en en cualquier momento.

(*) En el código multiproceso, puede ser necesario utilizar "barreras de memoria" para garantizar que antes de que cualquier subproceso pueda ver alguna referencia al contenedor, los efectos de todas las acciones en el contenedor serían visibles para ese subproceso, pero ese es un caso especial mencionado aquí solo para completar.

Super gato
fuente
gracias por una respuesta impresionante !! Creo que probablemente la fuente de mi confusión se debe a que soy de c # background y estoy aprendiendo a "escribir código de estilo funcional en c #", que sigue diciendo en todas partes evitar objetos mutables, pero creo que los lenguajes que adoptan el paradigma de programación funcional promueven (o imponen, no estoy seguro) si imponer es correcto usar) inmutabilidad.
rahulaga_dev
@RahulAgarwal: es posible tener referencias a un objeto que encapsulan un valor cuyo significado no se ve afectado por la existencia de otras referencias al mismo objeto, tener una identidad que las asocie con otras referencias al mismo objeto, o ninguna. Si el estado de la palabra real cambia, entonces el valor o la identidad de un objeto asociado con ese estado puede ser constante, pero no ambos: uno tendrá que cambiar. Los $ 50,000 es lo que debería hacer qué.
supercat
1

Como ya se mencionó, el problema con el estado mutable es básicamente una subclase del problema más grande de los efectos secundarios , donde el tipo de retorno de una función no describe con precisión lo que realmente hace la función, porque en este caso, también indica mutación. Este problema ha sido abordado por algunos nuevos lenguajes de investigación, como F * ( http://www.fstar-lang.org/tutorial/ ). Este lenguaje crea un Sistema de efectos similar al sistema de tipos, donde una función no solo declara estáticamente su tipo, sino también sus efectos. De esta forma, los llamadores de la función son conscientes de que puede producirse una mutación de estado al llamar a la función, y ese efecto se propaga a sus llamadores.

Aaron M. Eshbach
fuente