Puro funcional vs tell, no preguntes?

14

"El número ideal de argumentos para una función es cero" es simplemente incorrecto. El número ideal de argumentos es exactamente el número necesario para permitir que su función esté libre de efectos secundarios. Menos que eso e innecesariamente haces que tus funciones sean impuras, lo que te obliga a alejarte del pozo del éxito y subir el gradiente de dolor. A veces, "tío Bob" es acertado con su consejo. A veces está espectacularmente equivocado. Su consejo de cero argumentos es un ejemplo de esto último.

( Fuente: comentario de @David Arno bajo otra pregunta en este sitio )

El comentario ha ganado una cantidad espectacular de 133 votos a favor, por lo que me gustaría prestar más atención a su mérito.

Hasta donde yo sé, hay dos formas distintas en la programación: programación funcional pura (lo que este comentario es alentador) y decir, no preguntar (que de vez en cuando también se recomienda en este sitio web). AFAIK estos dos principios son fundamentalmente incompatibles, cerca de ser opuestos entre sí: funcional puro puede resumirse como "solo valores de retorno, no tienen efectos secundarios", mientras que decir, no preguntar puede resumirse como "no devolver nada, solo tiene efectos secundarios ". Además, estoy un poco perplejo porque pensé que decir, no preguntar se consideraba el núcleo del paradigma OO, mientras que las funciones puras se consideraban el núcleo del paradigma funcional: ¡ahora veo funciones puras recomendadas en OO!

Supongo que los desarrolladores probablemente deberían elegir uno de estos paradigmas y atenerse a él. Bueno, debo admitir que nunca podría obligarme a seguir tampoco. A menudo me parece conveniente devolver un valor y realmente no puedo ver cómo puedo lograr lo que quiero lograr solo con efectos secundarios. A menudo me parece conveniente tener efectos secundarios y realmente no puedo ver cómo puedo lograr lo que quiero lograr solo devolviendo valores. Además, a menudo (supongo que esto es horrible) tengo métodos que hacen ambas cosas.

Sin embargo, a partir de estos 133 votos a favor, estoy razonando que actualmente la programación funcional pura es "ganadora", ya que se convierte en un consenso de que es superior contar, no preguntar. ¿Es esto correcto?

Por lo tanto, en el ejemplo de este juego lleno de antipatrón que estoy tratando de hacer : si quisiera ponerlo en conformidad con el paradigma funcional puro, ¡¿CÓMO ?!

Me parece razonable tener un estado de batalla. Dado que este es un juego por turnos, mantengo los estados de batalla en un diccionario (multijugador; puede haber muchas batallas jugadas por muchos jugadores al mismo tiempo). Cada vez que un jugador hace su turno, llamo a un método apropiado en el estado de batalla que (a) modifica el estado en consecuencia y (b) devuelve actualizaciones a los jugadores, que se serializan en JSON y básicamente les digo lo que acaba de suceder en el tablero. Esto, supongo, es una violación flagrante de AMBOS principios y al mismo tiempo.

OK, podría hacer que un método DEVUELVA un estado de batalla en lugar de modificarlo en su lugar si realmente quisiera. ¡Pero! ¿Tendré que copiar todo innecesariamente en el estado de batalla solo para devolver un estado completamente nuevo en lugar de modificarlo en su lugar?

Ahora, tal vez si el movimiento es un ataque, ¿podría devolver un HP actualizado a los personajes? El problema es que no es tan simple: las reglas del juego, un movimiento puede y a menudo tendrá muchos más efectos que simplemente eliminar una parte del HP de un jugador. Por ejemplo, puede aumentar la distancia entre los personajes, aplicar efectos especiales, etc.

Parece mucho más simple para mí simplemente modificar el estado en su lugar y devolver actualizaciones ...

Pero, ¿cómo abordaría esto un ingeniero experimentado?

gaazkam
fuente
99
Seguir cualquier paradigma es un camino seguro al fracaso. La política nunca debe triunfar sobre la inteligencia. La solución a un problema debe depender del problema, no de sus creencias religiosas sobre la resolución de problemas.
John Douma
1
Nunca he tenido una pregunta aquí sobre algo que he dicho antes. Soy honrado. :)
David Arno

Respuestas:

14

Como la mayoría de los aforismos de programación, "decir, no preguntar" sacrifica la claridad para ganar brevedad. No tiene la intención de recomendar en contra de pedir los resultados de un cálculo, se recomienda no pedir las entradas de un cálculo. "No obtener, luego calcular, luego establecer, pero está bien devolver un valor de un cálculo", no es tan contundente.

Solía ​​ser bastante común que las personas llamaran a un captador, hicieran algunos cálculos y luego llamaran a un colocador con el resultado. Esta es una señal clara de que su cálculo en realidad pertenece a la clase a la que llamó getter. "Dígale, no pregunte" fue acuñado para recordarle a la gente que esté atento a ese antipatrón, y funcionó tan bien que ahora algunas personas piensan que esa parte es obvia y buscan otros tipos de "preguntas" para eliminar. Sin embargo, el aforismo solo se aplica útilmente a esa situación.

Los programas funcionales puros nunca sufrieron ese antipatrón exacto, por la sencilla razón de que no hay configuradores en ese estilo. Sin embargo, el problema más general (y más difícil de ver) de no mezclar diferentes niveles de abstracción semántica en la misma función se aplica a cada paradigma.

Karl Bielefeldt
fuente
Gracias por explicar correctamente "Dile, no preguntes".
user949300
13

Tanto el tío Bob como David Arno (el autor de la cita que tenía) tienen lecciones importantes que podemos deducir de lo que escribieron. Creo que vale la pena aprender la lección y luego extrapolar lo que eso realmente significa para usted y su proyecto.

Primero: la lección del tío Bob

El tío Bob está señalando que cuantos más argumentos tenga en su función / método, más entenderán los desarrolladores que lo usen. Esa carga cognitiva no es gratis, y si no eres coherente con el orden de los argumentos, etc., la carga cognitiva solo aumenta.

Ese es un hecho de ser humano. Creo que el error clave en el libro Clean Code del tío Bob es la declaración "El número ideal de argumentos para una función es cero" . El minimalismo es genial hasta que no lo es. Al igual que nunca alcanza sus límites en Cálculo, nunca alcanzará el código "ideal", ni debería hacerlo.

Como dijo Albert Einstein, "Todo debería ser lo más simple posible, pero no más simple".

Segundo: la lección de David Arno

La forma de desarrollar que describió David Arno es un desarrollo de estilo más funcional que orientado a objetos . Sin embargo, el código funcional escala mucho mejor que la programación tradicional orientada a objetos. ¿Por qué? Por bloqueo. Cada vez que el estado es mutable en un objeto, corre el riesgo de condiciones de carrera o bloqueo de contención.

Habiendo escrito sistemas altamente concurrentes utilizados en simulaciones y otras aplicaciones del lado del servidor, el modelo funcional funciona de maravilla. Puedo dar fe de las mejoras que ha realizado el enfoque. Sin embargo, es un estilo de desarrollo muy diferente, con diferentes requisitos y modismos.

El desarrollo es una serie de compensaciones

Conoces tu aplicación mejor que cualquiera de nosotros. Es posible que no necesite la escalabilidad que viene con la programación de estilo funcional. Hay un mundo entre los dos ideales mencionados anteriormente. Aquellos de nosotros que trabajamos con sistemas que necesitan manejar un alto rendimiento y un paralelismo ridículo tendiremos hacia el ideal de la programación funcional.

Dicho esto, puede usar objetos de datos para contener el conjunto de información que necesita pasar a un método. Eso ayuda con el problema de carga cognitiva que estaba abordando el tío Bob, al tiempo que respalda el ideal funcional que David Arno estaba abordando.

He trabajado tanto en sistemas de escritorio con un paralelismo limitado requerido como en un software de simulación de alto rendimiento. Tienen necesidades muy diferentes. Puedo apreciar un código orientado a objetos bien escrito que está diseñado en torno al concepto de ocultación de datos con el que está familiarizado. Funciona para varias aplicaciones. Sin embargo, no funciona para todos ellos.

¿Quién tiene la razón? Bueno, David tiene más razón que el tío Bob en este caso. Sin embargo, el punto subyacente que quiero subrayar aquí es que un método debe tener tantos argumentos como tenga sentido.

Berin Loritsch
fuente
Hay paralelismo. Se pueden procesar diferentes batallas en paralelo. Sin embargo, sí: una sola batalla, mientras se está procesando, debe bloquearse.
gaazkam
Sí, quise decir que los lectores (segadores en su analogía) recogerían de sus escritos (el sembrador). Dicho esto, he vuelto a mirar algunas cosas que he escrito en el pasado y he vuelto a aprender algo o no estoy de acuerdo con mi anterior yo. Todos estamos aprendiendo y evolucionando, y esa es la razón número uno por la que siempre debe razonar cómo y si aplica algo que aprendió.
Berin Loritsch
8

OK, podría hacer que un método DEVUELVA un estado de batalla en lugar de modificarlo en su lugar si realmente quisiera.

Sí, esa es la idea.

¿Tendré que copiar todo en el estado de batalla para devolver un estado completamente nuevo en lugar de modificarlo en su lugar?

No. Su "estado de batalla" podría modelarse como una estructura de datos inmutable, que contiene otras estructuras de datos inmutables como bloques de construcción, tal vez anidados en algunas jerarquías de estructuras de datos inmutables.

Por lo tanto, podría haber partes del estado de batalla que no tienen que cambiarse durante un turno, y otras que tienen que cambiarse. Las partes que no cambian no tienen que copiarse, ya que son inmutables, solo es necesario copiar una referencia a esas partes, sin ningún riesgo de introducir efectos secundarios. Eso funciona mejor en entornos de lenguaje recolectados de basura.

Busque "Estructuras de datos eficientes e inmutables" en Google, y seguramente encontrará algunas referencias de cómo funciona esto en general.

Parece mucho más simple para mí simplemente modificar el estado en su lugar y devolver actualizaciones.

Para ciertos problemas, esto puede ser más simple. Los juegos y las simulaciones basadas en rondas pueden caer dentro de esta categoría, dado que gran parte de los cambios de estado del juego de una ronda a otra. Sin embargo, la percepción de lo que es realmente "más simple" es hasta cierto punto subjetiva, y también depende mucho de lo que la gente esté acostumbrada.

Doc Brown
fuente
8

Como autor del comentario, creo que debería aclararlo aquí, ya que, por supuesto, hay más que la versión simplificada que ofrece mi comentario.

AFAIK estos dos principios son fundamentalmente incompatibles, cerca de ser opuestos entre sí: funcional puro puede resumirse como "solo valores de retorno, no tienen efectos secundarios", mientras que decir, no preguntar puede resumirse como "no devolver nada, solo tiene efectos secundarios ".

Para ser honesto, me parece un uso realmente extraño del término "decir, no preguntar". Así que leí lo que dijo Martin Fowler sobre el tema hace unos años, lo cual fue esclarecedor . La razón por la que lo encontré extraño es porque "decir no preguntar" es sinónimo de inyección de dependencia en mi cabeza y la forma más pura de inyección de dependencia es pasar todo lo que una función necesita a través de sus parámetros.

Pero parece que el significado que aplico a "decir, no preguntar" proviene de tomar la definición centrada en OO de Fowler y hacerla más agnóstica de paradigma. En el proceso, creo que lleva el concepto a sus conclusiones lógicas.

Volvamos a simples comienzos. Tenemos "bloques de lógica" (procedimientos) y tenemos datos globales. Los procedimientos leen esos datos directamente para acceder a ellos. Tenemos un escenario simple de "preguntar".

Viento hacia adelante un poco. Ahora tenemos objetos y métodos. Esos datos ya no necesitan ser globales, se pueden pasar a través del constructor y estar contenidos dentro del objeto. Y luego tenemos métodos que actúan sobre esos datos. Así que ahora tenemos "decir, no preguntar" como lo describe Fowler. El objeto recibe sus datos. Esos métodos ya no tienen que solicitar el alcance global de sus datos. Pero aquí está el problema: esto todavía no es cierto "decir, no preguntar" en mi opinión, ya que esos métodos todavía tienen que preguntar el alcance del objeto. Esto es más un escenario de "decir, luego preguntar" que siento.

Así que avance hacia el día moderno, deseche el enfoque de "todo está bajo" y tome prestados algunos principios de la programación funcional. Ahora, cuando se llama a un método, todos los datos se le proporcionan a través de sus parámetros. Se puede (y se ha) argumentado, "¿cuál es el punto, eso solo complica el código?" Y sí, pasar a través de parámetros, datos accesibles a través del alcance del objeto, agrega complejidad al código. Pero almacenar esos datos en un objeto, en lugar de hacerlo accesible globalmente, también agrega complejidad. Sin embargo, pocos argumentarían que las variables globales siempre son mejores porque son más simples. El punto es que los beneficios que "decir, no preguntar" son mayores que la complejidad de reducir el alcance. Esto se aplica más a pasar parámetros vía que restringir el alcance al objeto.private staticy pasa todo lo que necesita a través de los parámetros y ahora se puede confiar en que el método no accederá a escondidas cosas que no debería. Además, alienta a mantener el método pequeño, de lo contrario la lista de parámetros se saldrá de control. Y alienta los métodos de escritura que se ajustan a los criterios de "función pura".

Por lo tanto, no veo "funcional puro" y "decir, no preguntar" como opuestos entre sí. La primera es la única implementación completa de la segunda en lo que a mí respecta. El enfoque de Fowler no es completo "decir, no preguntar".

Pero es importante recordar que esta "implementación completa de tell don't ask" es realmente un ideal, es decir, el pragmatismo debe entrar en juego para que no nos volvamos idealistas y así tratarlo erróneamente como el único enfoque posiblemente correcto. Muy pocas aplicaciones pueden llegar a ser 100% libres de efectos secundarios por la simple razón de que no harían nada útil si realmente estuvieran libres de efectos secundarios. Necesitamos cambiar de estado, necesitamos IO, etc. para que la aplicación sea útil. Y en tales casos, los métodos deben causar efectos secundarios y, por lo tanto, no pueden ser puros. Pero la regla general aquí es mantener estos métodos "impuros" al mínimo; solo tienen efectos secundarios porque lo necesitan, en lugar de ser la norma.

Me parece razonable tener un estado de batalla. Dado que este es un juego por turnos, mantengo los estados de batalla en un diccionario (multijugador; puede haber muchas batallas jugadas por muchos jugadores al mismo tiempo). Cada vez que un jugador hace su turno, llamo a un método apropiado en el estado de batalla que (a) modifica el estado en consecuencia y (b) devuelve actualizaciones a los jugadores, que se serializan en JSON y básicamente les digo lo que acaba de suceder en el tablero.

Parece más que razonable tener un estado de batalla para mí; Parece esencial. El propósito completo de dicho código es manejar las solicitudes para cambiar el estado, administrar esos cambios de estado e informarlos. Podría manejar ese estado globalmente, podría mantenerlo dentro de objetos de jugador individuales o podría pasarlo por un conjunto de funciones puras. El que elija se reduce a cuál funciona mejor para su escenario particular. El estado global simplifica el diseño del código y es rápido, lo cual es un requisito clave de la mayoría de los juegos. Pero hace que el código sea mucho más difícil de mantener, probar y depurar. Un conjunto de funciones puras hará que el código sea más complejo de implementar y corre el riesgo de que sea demasiado lento debido a la copia excesiva de datos. Pero será el más simple de probar y mantener. El "enfoque OO" se encuentra a medio camino.

La clave es: no hay una solución perfecta que funcione todo el tiempo. El objetivo de las funciones puras es ayudarlo a "caer en el pozo del éxito". Pero si ese pozo es tan superficial, debido a la complejidad que puede aportar al código, que no caes tanto en él como si te tropiezas con él, entonces no es el enfoque adecuado para ti. Apunte al ideal, pero sea pragmático y pare cuando ese ideal no sea un buen lugar para ir esta vez.

Y como punto final, solo para reiterar: las funciones puras y "decir, no preguntar" no son opuestos en absoluto.

David Arno
fuente
5

Para cualquier cosa, alguna vez dicho, existe un contexto, en el que puede poner esa declaración, que lo hará absurdo.

ingrese la descripción de la imagen aquí

El tío Bob está completamente equivocado si tomas el consejo de argumento cero como un requisito. Tiene toda la razón si entiendes que cada argumento adicional hace que el código sea más difícil de leer. Viene con un costo. No agrega argumentos a las funciones porque las hace más fáciles de leer. Agrega argumentos a las funciones porque no puede pensar en un buen nombre que haga evidente la dependencia de ese argumento.

Por ejemplo, pi()es una función perfectamente buena como es. ¿Por qué? Porque no me importa cómo, o incluso si, se calculó. O si usó e, o sin (), para llegar al número que devuelve. Estoy de acuerdo porque el nombre me dice todo lo que necesito saber.

Sin embargo, no todos los nombres me dicen todo lo que necesito saber. Algunos nombres no revelan información importante para comprender que controla el comportamiento de la función, al igual que los argumentos expuestos. Eso es lo que hace que el estilo funcional de la programación sea más fácil de razonar.

Puedo mantener las cosas inmutables y sin efectos secundarios en un estilo completamente OOP. El retorno es simplemente una mecánica utilizada para dejar valores en la pila para el siguiente procedimiento. Puede permanecer igual de inmutable utilizando puertos de salida para comunicar valores a otras cosas inmutables hasta que llegue al último puerto de salida que finalmente tiene que cambiar algo si desea que las personas puedan leerlo. Eso es cierto para todos los idiomas, funcionales o no.

Por lo tanto, no afirme que la programación funcional y la programación orientada a objetos son "fundamentalmente incompatibles". Puedo usar objetos en mis programas funcionales y puedo usar funciones puras en mis programas OO.

Sin embargo, hay un costo para mezclarlos: expectativas. Puedes seguir fielmente la mecánica de ambos paradigmas y aún así causar confusión. Uno de los beneficios de usar un lenguaje funcional es que los efectos secundarios, aunque deben existir para obtener cualquier salida, se colocan en un lugar predecible. A menos que, por supuesto, se acceda a un objeto mutable de manera indisciplinada. Entonces, lo que tomaste como algo dado en ese idioma se desmorona.

Del mismo modo, puede admitir objetos con funciones puras, puede diseñar objetos que sean inmutables. El problema es que si no indica que las funciones son puras o que los objetos son inmutables, las personas no obtendrán ningún beneficio de razonamiento de esas características hasta que hayan pasado mucho tiempo leyendo el código.

Esto no es un problema nuevo. Durante años, las personas se han codificado procesalmente en "idiomas OO" pensando que están haciendo OO porque usan un "lenguaje OO". Pocos idiomas son tan buenos para evitar que te dispares en el pie. Para que estas ideas funcionen, tienen que vivir en ti.

Ambos proporcionan buenas características. Puedes hacer las dos cosas. Si eres lo suficientemente valiente como para mezclarlos, etiquétalos claramente.

naranja confitada
fuente
0

A veces me cuesta entender todas las reglas de varios paradigmas. A veces están en desacuerdo entre sí, ya que están en esta situación.

OOP es un paradigma imperativo que se trata de correr con tijeras en el mundo donde suceden cosas peligrosas.

FP es un paradigma funcional en el que uno encuentra seguridad absoluta en la computación pura. Aquí no pasa nada.

Sin embargo, todos los programas deben unirse al mundo imperativo para ser útiles. Por lo tanto, núcleo funcional, caparazón imperativo .

Las cosas se vuelven confusas cuando comienzas a definir objetos inmutables (aquellos cuyos comandos devuelven una copia modificada en lugar de mutar). Te dices a ti mismo: "Esto es POO" y "Estoy definiendo el comportamiento del objeto". Piensa de nuevo en el probado y probado principio Tell, Don't Ask. El problema es que lo estás aplicando al reino equivocado.

Los reinos son completamente diferentes y cumplen diferentes reglas. El reino funcional se acumula hasta el punto en que quiere liberar efectos secundarios en el mundo. Para que se publiquen esos efectos, todos los datos que se habrían encapsulado en un objeto imperativo (¡si se hubiera escrito de esa manera!) Deben estar disponibles para el shell imperativo. Sin acceso a estos datos que en un mundo diferente se habrían ocultado mediante encapsulación, no puede hacer el trabajo. Es computacionalmente imposible.

Por lo tanto, cuando esté escribiendo objetos inmutables (lo que Clojure llama estructuras de datos persistentes) recuerde que está en el dominio funcional. Lanza Tell, Don't Ask por la ventana y deja que vuelva a entrar en la casa solo cuando vuelvas a entrar en el reino imperativo.

Mario T. Lanza
fuente