OK, entonces el título es un poco clickbaity, pero en serio he estado diciendo, no pidas patada por un tiempo. Me gusta cómo fomenta que los métodos se utilicen como mensajes de manera orientada a objetos. Pero esto tiene un problema persistente que ha estado dando vueltas en mi cabeza.
He llegado a sospechar que un código bien escrito puede seguir los principios OO y los principios funcionales al mismo tiempo. Estoy intentando conciliar estas ideas y el gran escollo que he aterrizado en es return
.
Una función pura tiene dos cualidades:
Llamarlo repetidamente con las mismas entradas siempre da el mismo resultado. Esto implica que es inmutable. Su estado se establece solo una vez.
No produce efectos secundarios. El único cambio causado al llamarlo es producir el resultado.
Entonces, ¿cómo hace uno para ser puramente funcional si usted ha renunciado a utilizar return
como su forma de comunicar los resultados?
La idea tell, no ask funciona usando lo que algunos considerarían un efecto secundario. Cuando trato con un objeto no le pregunto sobre su estado interno. Le digo lo que debo hacer y utiliza su estado interno para averiguar qué hacer con lo que le dije que hiciera. Una vez que lo cuento, no pregunto qué hizo. Solo espero que haya hecho algo sobre lo que le dijeron que hiciera.
Pienso en Tell, Don't Ask como algo más que un nombre diferente para la encapsulación. Cuando uso return
no tengo idea de lo que me llamó. No puedo hablar de su protocolo, tengo que obligarlo a tratar con mi protocolo. Que en muchos casos se expresa como el estado interno. Incluso si lo que está expuesto no es exactamente el estado, generalmente es solo un cálculo realizado en el estado y los argumentos de entrada. Tener una interfaz para responder ofrece la oportunidad de combinar los resultados en algo más significativo que el estado interno o los cálculos. Ese es el mensaje que pasa . Ver este ejemplo .
En el pasado, cuando las unidades de disco en realidad tenían discos y una unidad de memoria USB era lo que hacía en el automóvil cuando la rueda estaba demasiado fría para tocarla con los dedos, me enseñaron cómo las personas molestas consideran las funciones que no tienen parámetros. void swap(int *first, int *second)
Parecía muy útil, pero nos animaron a escribir funciones que devolvieran los resultados. Entonces tomé esto en serio por fe y comencé a seguirlo.
Pero ahora veo personas que construyen arquitecturas donde los objetos permiten que la forma en que fueron construidas controlen dónde envían sus resultados. Aquí hay un ejemplo de implementación . Inyectar el objeto del puerto de salida parece un poco como la idea del parámetro out nuevamente. Pero así es como los objetos tell-don't-ask le dicen a otros objetos lo que han hecho.
Cuando supe por primera vez acerca de los efectos secundarios, pensé que era el parámetro de salida. Nos dijeron que no sorprendiéramos a las personas al hacer que parte del trabajo se llevara a cabo de una manera sorprendente, es decir, al no seguir la return result
convención. Ahora claro, sé que hay una pila de problemas de subprocesos asincrónicos paralelos con los que los efectos secundarios se resuelven, pero el retorno es realmente solo una convención que hace que dejes el resultado en la pila para que lo que llames puedas sacarlo más tarde. Eso es todo lo que realmente es.
Lo que realmente estoy tratando de preguntar:
¿Es return
la única manera de evitar toda la miseria de los efectos secundarios y obtener seguridad para el hilo sin bloqueos, etc. O puedo seguir diciéndole, no pregunte de una manera puramente funcional?
fuente
Respuestas:
Si una función no tiene ningún efecto secundario y no devuelve nada, entonces la función es inútil. Es tan simple como eso.
Pero supongo que puedes usar algunos trucos si quieres seguir la letra de las reglas e ignorar el razonamiento subyacente. Por ejemplo, usar un parámetro out es, estrictamente hablando, no usar un retorno. Pero todavía hace exactamente lo mismo que un retorno, solo que de una manera más complicada. Entonces, si cree que el retorno es malo por alguna razón , entonces usar un parámetro out es claramente malo por las mismas razones subyacentes.
Puedes usar más trucos complicados. Por ejemplo, Haskell es famoso por el truco de la mónada IO en el que puede tener efectos secundarios en la práctica, pero aún así, estrictamente hablando, no tiene efectos secundarios desde un punto de vista teórico. El estilo de paso continuo es otro truco, que le permite evitar devoluciones al precio de convertir su código en espagueti.
La conclusión es que, en ausencia de trucos tontos, los dos principios de las funciones libres de efectos secundarios y "sin retorno" simplemente no son compatibles. Además, señalaré que ambos son principios realmente malos (dogmas realmente) en primer lugar, pero esa es una discusión diferente.
Reglas como "decir, no preguntar" o "sin efectos secundarios" no se pueden aplicar universalmente. Siempre tienes que considerar el contexto. Un programa sin efectos secundarios es literalmente inútil. Incluso los lenguajes funcionales puros lo reconocen. Más bien se esfuerzan por separar las partes puras del código de las que tienen efectos secundarios. El objetivo de las mónadas estatales o de E / S en Haskell no es que evites los efectos secundarios, porque no puedes, sino que la presencia de los efectos secundarios está explícitamente indicada por la firma de la función.
La regla tell-dont-ask se aplica a un tipo diferente de arquitectura: el estilo en el que los objetos del programa son "actores" independientes que se comunican entre sí. Cada actor es básicamente autónomo y encapsulado. Puede enviarle un mensaje y decide cómo reaccionar, pero no puede examinar el estado interno del actor desde el exterior. Esto significa que no puede saber si un mensaje cambia el estado interno del actor / objeto. El estado y los efectos secundarios están ocultos por diseño .
fuente
Tell, Don't Ask viene con algunos supuestos fundamentales:
Ninguna de estas cosas se aplica a las funciones puras.
Así que repasemos por qué tenemos la regla "Dile, no preguntes". Esta regla es una advertencia y un recordatorio. Se puede resumir así:
Para decirlo de otra manera, las clases son las únicas responsables de mantener su propio estado y actuar en consecuencia. De esto se trata la encapsulación.
De Fowler :
Para reiterar, nada de esto tiene nada que ver con funciones puras, o incluso impuras, a menos que exponga el estado de una clase al mundo exterior. Ejemplos:
Infracción de TDA
No es una violación de TDA
o
En los dos últimos ejemplos, el semáforo retiene el control de su estado y sus acciones.
fuente
No solo pregunta por su estado interno , tampoco pregunta si tiene un estado interno .
¡También diga, no pregunte! no no implicaría no conseguir un resultado en forma de un valor de retorno (proporcionado por una
return
declaración dentro del método). Simplemente implica que no me importa cómo lo haces, ¡pero haz ese procesamiento! . Y a veces quieres inmediatamente el resultado del procesamiento ...fuente
next()
método de iteradores no solo debe devolver el objeto actual sino también cambiar el estado interno de los iteradores para que la próxima llamada devuelva el siguiente objeto ...Si considera
return
"dañino" (permanecer en su imagen), entonces en lugar de hacer una función comocompilarlo de manera que pase mensajes:
Mientras
f
, yg
son libre de efectos secundarios, encadenar juntos será libre de efectos secundarios también. Creo que este estilo es similar a lo que también se llama estilo de paso continuo .Si esto realmente conduce a programas "mejores" es discutible, ya que rompe algunas convenciones. El ingeniero de software alemán Ralf Westphal hizo todo un modelo de programación en torno a esto, lo llamó "Componentes basados en eventos" con una técnica de modelado que llama "Diseño de flujo".
Para ver algunos ejemplos, comience en la sección "Traducir a eventos" de esta entrada de blog . Para el enfoque completo, recomiendo su libro electrónico "Mensajería como modelo de programación - Hacer POO como si lo quisieras" .
fuente
If this really leads to "better" programs is debatable
Solo tenemos que mirar el código escrito en JavaScript durante la primera década de este siglo. Jquery y sus complementos eran propensos a este paradigma de devoluciones de llamada ... devoluciones de llamada en todas partes . En cierto punto, demasiadas devoluciones de llamadas anidadas , hicieron que la depuración fuera una pesadilla. El código aún debe ser leído por los humanos, independientemente de las excentricidades de la ingeniería del software y sus "principios"return
datos de las funciones y seguir adhiriéndose a Tell Don't Ask, siempre que no controle el estado de un objeto.La transmisión de mensajes es inherentemente efectiva. Si le dice a un objeto que haga algo, espera que tenga un efecto en algo. Si el controlador de mensajes era puro, no necesitaría enviarle un mensaje.
En los sistemas de actores distribuidos, el resultado de una operación generalmente se envía como un mensaje al remitente de la solicitud original. El remitente del mensaje está implícitamente disponible por el tiempo de ejecución del actor o (por convención) se pasa explícitamente como parte del mensaje. Al pasar mensajes sincrónicos, una sola respuesta es similar a una
return
declaración. En el paso de mensajes asíncronos, el uso de mensajes de respuesta es particularmente útil, ya que permite el procesamiento concurrente en múltiples actores sin dejar de ofrecer resultados.Pasar el "remitente" al que se debe entregar el resultado explícitamente básicamente modela el estilo de paso de continuación o los parámetros temidos, excepto que les pasa mensajes a ellos en lugar de mutarlos directamente.
fuente
Toda esta pregunta me parece una 'violación de nivel'.
Tiene (al menos) los siguientes niveles en un proyecto importante:
Y así sucesivamente hasta fichas individuales.
Realmente no hay necesidad de que una entidad en el nivel de método / función no regrese (incluso si solo regresa
this
). Y no hay (en su descripción) ninguna necesidad de que una entidad en el nivel de Actor devuelva nada (dependiendo del lenguaje que ni siquiera sea posible). Creo que la confusión está en combinar esos dos niveles, y diría que deberían razonarse claramente (incluso si cualquier objeto dado realmente abarca múltiples niveles).fuente
Usted menciona que desea ajustarse tanto al principio OOP de "decir, no preguntar" como al principio funcional de las funciones puras, pero no entiendo cómo eso lo llevó a evitar la declaración de devolución.
Una forma alternativa relativamente común de seguir estos dos principios es ir todo en las declaraciones de retorno y usar objetos inmutables solo con getters. El enfoque es que algunos de los captadores devuelvan un objeto similar con un nuevo estado, en lugar de cambiar el estado del objeto original.
Un ejemplo de este enfoque está en los tipos de datos
tuple
y Python incorporadosfrozenset
. Aquí hay un uso típico de un conjunto congelado:Lo que imprimirá lo siguiente, demostrando que el método de unión crea un nuevo conjunto congelado con su propio estado sin afectar los objetos antiguos:
Otro extenso ejemplo de estructuras de datos inmutables similares es la biblioteca Immutable.js de Facebook . En ambos casos, comienza con estos bloques de construcción y puede construir objetos de dominio de nivel superior que siguen los mismos principios, logrando un enfoque OOP funcional, que lo ayuda a encapsular los datos y razonar sobre ellos más fácilmente. Y la inmutabilidad también le permite obtener el beneficio de poder compartir dichos objetos entre hilos sin tener que preocuparse por los bloqueos.
fuente
He estado haciendo todo lo posible para conciliar algunos de los beneficios de, más específicamente, la programación imperativa y funcional (naturalmente, no obteniendo todos los beneficios, pero tratando de obtener la mayor parte de ambos), aunque en
return
realidad es fundamental para hacerlo en Una manera sencilla para mí en muchos casos.Con respecto a tratar de evitar
return
declaraciones directamente, traté de reflexionar sobre esto durante la última hora más o menos y básicamente la pila desbordó mi cerebro varias veces. Puedo ver su atractivo en términos de imponer el nivel más fuerte de encapsulación y ocultación de información a favor de objetos muy autónomos a los que simplemente se les dice qué hacer, y me gusta explorar las extremidades de las ideas aunque solo sea para tratar de obtener un mejor comprensión de cómo funcionan.Si utilizamos el ejemplo del semáforo, inmediatamente un intento ingenuo querría dar a ese semáforo un conocimiento del mundo entero que lo rodea, y eso ciertamente sería indeseable desde una perspectiva de acoplamiento. Entonces, si entiendo correctamente, lo abstrae y desacopla a favor de generalizar el concepto de puertos de E / S que propagan aún más los mensajes y las solicitudes, no los datos, a través de la tubería, y básicamente inyectan estos objetos con las interacciones / solicitudes deseadas entre sí mientras ajenos el uno al otro.
La tubería nodal
Y ese diagrama es casi tan lejos como traté de esbozar esto (y aunque simple, tuve que seguir cambiándolo y repensándolo). Inmediatamente tiendo a pensar que un diseño con este nivel de desacoplamiento y abstracción se volvería muy difícil de razonar en forma de código, porque el orquestador (es) que conecta todas estas cosas para un mundo complejo podría encontrarlo muy difícil. realice un seguimiento de todas estas interacciones y solicitudes para crear la canalización deseada. Sin embargo, en forma visual, podría ser razonablemente sencillo dibujar estas cosas como un gráfico y vincular todo y ver que las cosas suceden de manera interactiva.
En términos de efectos secundarios, podría ver que esto está libre de "efectos secundarios" en el sentido de que estas solicitudes podrían, en la pila de llamadas, conducir a una cadena de comandos para que cada hilo se ejecute, por ejemplo (no cuento esto como un "efecto secundario" en un sentido pragmático, ya que no está alterando ningún estado relevante para el mundo exterior hasta que dichos comandos se ejecuten realmente; el objetivo práctico para mí en la mayoría de los programas no es eliminar los efectos secundarios, sino diferirlos y centralizarlos) . Y, además, la ejecución del comando podría generar un mundo nuevo en lugar de mutar el existente. Sin embargo, mi cerebro está realmente agotado al tratar de comprender todo esto, sin ningún intento de crear prototipos de estas ideas. Yo tampoco
Cómo funciona
Entonces, para aclarar, estaba imaginando cómo se programa realmente esto. La forma en que lo veía funcionar era en realidad el diagrama anterior que capturaba el flujo de trabajo del usuario final (programador). Puede arrastrar un semáforo al mundo, arrastrar un temporizador, darle un período transcurrido (al "construirlo"). El temporizador tiene un
On Interval
evento (puerto de salida), puede conectarlo al semáforo para que, en tales eventos, le indique a la luz que pase por sus colores.El semáforo podría entonces, al cambiar a ciertos colores, emitir salidas (eventos) como,
On Red
en ese punto, podríamos arrastrar a un peatón a nuestro mundo y hacer que ese evento le diga al peatón que comience a caminar ... o podríamos arrastrar pájaros hacia nuestra escena y hacerlo así, cuando la luz se pone roja, les decimos a las aves que comiencen a volar y batir sus alas ... o tal vez cuando la luz se pone roja, le decimos a una bomba que explote, lo que queramos, y con los objetos siendo completamente ajenos el uno al otro, y sin hacer nada más que indirectamente diciéndose mutuamente qué hacer a través de este concepto abstracto de entrada / salida.Y encapsulan completamente su estado y no revelan nada al respecto (a menos que estos "eventos" se consideren TMI, en ese punto tendré que repensar mucho las cosas), se dicen mutuamente cosas que deben hacer indirectamente, no preguntan. Y están súper desacoplados. Nada sabe nada excepto esta abstracción generalizada de puertos de entrada / salida.
Casos de uso práctico?
Pude ver que este tipo de cosas son útiles como un lenguaje incrustado específico de dominio de alto nivel en ciertos dominios para orquestar todos estos objetos autónomos que no saben nada sobre el mundo circundante, no exponen nada de su estado interno posterior a la construcción, y básicamente solo propagan solicitudes entre nosotros que podemos cambiar y ajustar al contenido de nuestros corazones. En este momento siento que esto es muy específico del dominio, o tal vez simplemente no he pensado lo suficiente, porque es muy difícil para mí pensar en los tipos de cosas que desarrollo regularmente (a menudo trabajo con código de nivel medio bajo) si tuviera que interpretar Tell, Don't Ask a tales extremos y quisiera el nivel más fuerte de encapsulación imaginable. Pero si estamos trabajando con abstracciones de alto nivel en un dominio específico,
Señales y Ranuras
Este diseño me pareció extrañamente familiar hasta que me di cuenta de que son básicamente señales y ranuras si no tomamos en cuenta los matices de cómo se implementa. La pregunta principal para mí es qué tan efectivamente podemos programar estos nodos individuales (objetos) en el gráfico como estrictamente adheridos a Tell, Don't Ask, llevados al grado de evitar
return
declaraciones, y si podemos evaluar dicho gráfico sin mutaciones (en paralelo, por ejemplo, bloqueo ausente). Ahí es donde están los beneficios mágicos no en cómo conectamos estas cosas potencialmente, sino en cómo se pueden implementar a este grado de mutaciones ausentes de encapsulación. Ambos me parecen factibles, pero no estoy seguro de cuán ampliamente aplicable sería, y ahí es donde estoy un poco perplejo tratando de resolver posibles casos de uso.fuente
Claramente veo fuga de certeza aquí. Parece que "efecto secundario" es un término bien conocido y comúnmente entendido, pero en realidad no lo es. Dependiendo de sus definiciones (que en realidad faltan en el OP), los efectos secundarios pueden ser totalmente necesarios (como @JacquesB logró explicar), o ser aceptados sin piedad. O, dando un paso hacia la aclaración, es necesario distinguir entre los efectos secundarios deseados que a uno no le gusta ocultar (en este punto emerge el famoso IO de Haskell: no es más que una forma de ser explícito) y los efectos secundarios no deseados como un resultado de errores de código y ese tipo de cosas . Esos son problemas bastante diferentes y, por lo tanto, requieren un razonamiento diferente.
Por lo tanto, sugiero comenzar a reformular: "¿Cómo definimos el efecto secundario y qué dicen las definiciones dadas sobre su interrelación con la declaración" return "?".
fuente