En la página 839 de la segunda edición, Steve McConnell está discutiendo todas las formas en que los programadores pueden "conquistar la complejidad" en grandes programas. Sus consejos culminan con esta declaración:
"La programación orientada a objetos proporciona un nivel de abstracción que se aplica a algoritmos y datos al mismo tiempo , un tipo de abstracción que la descomposición funcional por sí sola no proporcionó".
Junto con su conclusión de que "reducir la complejidad es posiblemente la clave más importante para ser un programador efectivo" (misma página), esto parece ser un desafío para la programación funcional.
El debate entre FP y OO a menudo está enmarcado por los proponentes de FP en torno a los problemas de complejidad que se derivan específicamente de los desafíos de concurrencia o paralelización. Pero la concurrencia ciertamente no es el único tipo de complejidad que los programadores de software necesitan conquistar. Quizás enfocarse en reducir un tipo de complejidad aumenta enormemente en otras dimensiones, de modo que en muchos casos, la ganancia no vale el costo.
Si cambiamos los términos de la comparación entre FP y OO de cuestiones particulares como la concurrencia o la reutilización a la gestión de la complejidad global, ¿cómo se vería ese debate?
EDITAR
El contraste que quería destacar es que OO parece encapsular y abstraerse de la complejidad de los datos y los algoritmos, mientras que la programación funcional parece alentar a los detalles de implementación de las estructuras de datos más "expuestos" en todo el programa.
Véase, por ejemplo, Stuart Halloway (un defensor de Clojure FP) aquí diciendo que "la sobreespecificación de los tipos de datos" es "una consecuencia negativa del estilo idiomático de OO" y está a favor de conceptualizar una libreta de direcciones como un simple vector o mapa en lugar de un objeto de OO más rico con propiedades y métodos adicionales (no vectoriales y no cartográficos). (Además, los defensores del diseño orientado a dominios y OO pueden decir que exponer una libreta de direcciones como un vector o mapa sobreexpone los datos encapsulados a métodos que son irrelevantes o incluso peligrosos desde el punto de vista del dominio).
Respuestas:
Tenga en cuenta que el libro fue escrito hace más de 20 años. Para los programadores profesionales de la época, FP no existía, estaba enteramente en el ámbito de académicos e investigadores.
Necesitamos enmarcar la "descomposición funcional" en el contexto apropiado del trabajo. El autor no se refiere a la programación funcional. Necesitamos vincular esto con la "programación estructurada" y el
GOTO
desorden lleno que vino antes. Si su punto de referencia es un viejo FORTRAN / COBOL / BASIC que no tenía funciones (tal vez, si tuviera suerte, obtendría un solo nivel de GOSUB) y todas sus variables son globales, pudiendo desglosar su programa en capas de funciones es una gran ayuda.OOP es un refinamiento adicional en este tipo de 'descomposición funcional'. No solo puede agrupar instrucciones en funciones, sino que también puede agrupar funciones relacionadas con los datos en los que están trabajando. El resultado es una pieza de código claramente definida que puede ver y comprender (idealmente) sin tener que perseguir todo su código base para encontrar qué más podría operar en sus datos.
fuente
Me imagino que los defensores de la programación funcional argumentarían que la mayoría de los lenguajes FP proporcionan más medios de abstracción que la "descomposición funcional sola" y, de hecho, permiten medios de abstracciones comparables en poder a los de los lenguajes orientados a objetos. Por ejemplo, uno podría citar las clases de tipo de Haskell o los módulos de orden superior de ML como tales medios de abstracción. Por lo tanto, la declaración (que estoy bastante seguro era sobre la orientación a objetos frente a la programación de procedimientos, no la programación funcional) no se aplica a ellos.
También debe señalarse que FP y OOP son conceptos ortogonales y no mutuamente excluyentes. Por lo tanto, no tiene sentido compararlos entre sí. Podría muy bien comparar "OOP imperativo" (por ejemplo, Java) versus "OOP funcional" (por ejemplo, Scala), pero la declaración que citó no se aplicaría a esa comparación.
fuente
Encuentro la programación funcional extremadamente útil en el manejo de la complejidad. Sin embargo, tiende a pensar en la complejidad de una manera diferente, definiéndola como funciones que actúan sobre datos inmutables en diferentes niveles en lugar de encapsular en un sentido de OOP.
Por ejemplo, recientemente escribí un juego en Clojure, y todo el estado del juego se definió en una sola estructura de datos inmutable:
Y el bucle principal del juego podría definirse como la aplicación de algunas funciones puras al estado del juego en un bucle:
La función clave llamada es
update-game
, que ejecuta un paso de simulación dado un estado del juego anterior y alguna entrada del usuario, y devuelve el nuevo estado del juego.Entonces, ¿dónde está la complejidad? En mi opinión, se ha gestionado bastante bien:
OOP también puede gestionar la complejidad a través de la encapsulación, pero si compara esto con OOP, el funcional tiene algunas ventajas muy importantes:
Finalmente, para las personas que están interesadas en obtener más información sobre cómo gestionar la complejidad en lenguajes funcionales frente a OOP, recomiendo encarecidamente el video del discurso de apertura de Rich Hickey Simple Made Easy (filmado en la conferencia de tecnología Strange Loop )
fuente
La descomposición funcional por sí sola no es suficiente para crear ningún tipo de algoritmo o programa: también debe representar los datos. Creo que la afirmación anterior supone implícitamente (o al menos se puede entender así) que los "datos" en el caso funcional son del tipo más rudimentario: solo listas de símbolos y nada más. La programación en tal lenguaje obviamente no es muy conveniente. Sin embargo, muchos, especialmente los lenguajes nuevos y modernos, funcionales (o multiparadigm), como Clojure, ofrecen estructuras de datos ricas: no solo listas, sino también cadenas, vectores, mapas y conjuntos, registros, estructuras y objetos. - Con metadatos y polimorfismo.
El gran éxito práctico de las abstracciones OO difícilmente puede ser discutido. ¿Pero es la última palabra? Como escribió, los problemas de concurrencia ya son el mayor problema, y el OO clásico no contiene ninguna idea de concurrencia en absoluto. Como resultado, las soluciones OO de facto para lidiar con la concurrencia son solo cinta adhesiva superpuesta: funciona, pero es fácil de arruinar, quita una cantidad considerable de recursos cerebrales de la tarea esencial en cuestión, y no escala bien. Tal vez sea posible tomar lo mejor de muchos mundos. Eso es lo que persiguen los lenguajes multiparadigm modernos.
fuente
El estado mutable es la raíz de la mayoría de las complejidades y problemas relacionados con la programación y el diseño de software / sistema.
OO abarca el estado mutable. FP aborrece el estado mutable.
Tanto OO como FP tienen sus usos y puntos dulces. Elegir sabiamente. Y recuerde el dicho: "Los cierres son los objetos del pobre. Los objetos son el cierre del pobre".
fuente
La programación funcional puede tener objetos, pero esos objetos tienden a ser inmutables. Las funciones puras (funciones sin efectos secundarios) luego operan en esas estructuras de datos. Es posible hacer objetos inmutables en lenguajes de programación orientados a objetos, pero no fueron diseñados para hacerlo y no es así como tienden a usarse. Esto dificulta razonar sobre programas orientados a objetos.
Tomemos un ejemplo muy simple. Digamos que Oracle decidió que las cadenas de Java deberían tener un método inverso y usted escribió el siguiente código.
¿Qué evalúa la última línea? Necesita un conocimiento especial de la clase String para saber que esto se evaluaría como falso.
¿Qué pasa si hice mi propia clase WuHoString
Es imposible saber a qué se evalúa la última línea.
En un estilo de programación funcional, se escribiría más de la siguiente manera:
Y debería ser cierto.
Si 1 función en una de las clases más básicas es tan difícil de razonar, entonces uno se pregunta si la introducción de esta idea de objetos mutables ha aumentado o disminuido la complejidad.
Obviamente, hay todo tipo de definiciones de lo que constituye orientado a objetos y lo que significa ser funcional y lo que significa tener ambos. Para mí, puede tener un "estilo de programación funcional" en lenguajes que no tienen funciones como las de primera clase, pero que están hechos para otros lenguajes.
fuente
Creo que en la mayoría de los casos la abstracción clásica de OOP no cubre la complejidad de concurrencia. Por lo tanto, OOP (por su significado original) no excluye FP, y es por eso que vemos cosas como scala.
fuente
La respuesta depende del idioma. Los Lisps, por ejemplo, tienen el aspecto realmente correcto de que el código son datos: ¡los algoritmos que escribes son solo listas de Lisp! Almacena los datos de la misma manera que escribe el programa. Esta abstracción es a la vez más simple y más exhaustiva que OOP y le permite hacer cosas realmente interesantes (consulte macros).
Haskell (y un lenguaje similar, me imagino) tienen una respuesta completamente diferente: tipos de datos algebraicos. Un tipo de datos algebraicos es como una
C
estructura, pero con más opciones. Estos tipos de datos proporcionan la abstracción necesaria para modelar datos; Las funciones proporcionan la abstracción necesaria para modelar algoritmos. Las clases de tipos y otras características avanzadas proporcionan un nivel aún más alto de abstracción sobre ambas.Por ejemplo, estoy trabajando en un lenguaje de programación llamado TPL por diversión. Los tipos de datos algebraicos hacen que sea realmente fácil representar valores:
Lo que esto dice, de una manera muy visual, es que un TPLValue (cualquier valor en mi idioma) puede ser un
Null
o unNumber
con unInteger
valor o incluso unFunction
con una lista de valores (los parámetros) y un valor final (el cuerpo )A continuación, puedo usar clases de tipo para codificar algunos comportamientos comunes. Por ejemplo, podría hacer una
TPLValue
instancia de loShow
que significa que se puede convertir en una cadena.Además, puedo usar mis propias clases de tipos cuando necesito especificar el comportamiento de ciertos tipos (incluidos los que no implementé yo mismo). Por ejemplo, tengo una
Extractable
clase de tipo que me permite escribir una función que tomaTPLValue
ay devuelve un valor normal apropiado. Porextract
lo tanto, puede convertir aNumber
aInteger
o aString
aString
siempreInteger
y cuandoString
sean instancias deExtractable
.Finalmente, la lógica principal de mi programa está en varias funciones como
eval
yapply
. Estos son realmente el núcleo: tomanTPLValue
s y los convierten en másTPLValue
s, además de manejar el estado y los errores.En general, las abstracciones que estoy usando en mi código Haskell son en realidad más poderosas que las que hubiera usado en un lenguaje OOP.
fuente
eval
. "¡Oye, mírame! ¡No necesito escribir mis propios agujeros de seguridad; tengo una vulnerabilidad de ejecución de código arbitraria integrada en el lenguaje de programación!" La combinación de datos con código es la causa raíz de una de las dos clases más populares de vulnerabilidades de seguridad de todos los tiempos. Cada vez que ves que alguien es pirateado debido a un ataque de inyección SQL (entre muchas otras cosas) es porque algún programador no sabe cómo separar adecuadamente los datos del código.eval
no depende mucho de la estructura de Lisp: puede tenerloeval
en lenguajes como JavaScript y Python. El verdadero poder viene de escribir macros, que son básicamente programas que actúan sobre programas como datos y generan otros programas. Esto hace que el lenguaje sea muy flexible y que sea fácil crear abstracciones poderosas.and
. cortocircuitosor
.let
.let-rec
.cond
.defn
. Ninguno de estos puede implementarse con funciones en lenguajes de orden aplicativo.for
(lista de comprensiones).dotimes
.doto
.and
!" Escuché, "oye, mírame, ¡mi lenguaje está tan paralizado que ni siquiera viene con un cortocircuitoand
y tengo que reinventar la rueda para todo !"La oración citada ya no tiene ninguna validez, por lo que puedo ver.
Los lenguajes OO contemporáneos no pueden abstraer sobre tipos cuyo tipo no es *, es decir, se desconocen los tipos de tipo superior. Su sistema de tipos no permite expresar la idea de "algún contenedor con elementos Int, que permite mapear una función sobre los elementos".
Por lo tanto, esta función básica como Haskells
no se puede escribir fácilmente en Java *), por ejemplo, al menos no de forma segura. Por lo tanto, para obtener una funcionalidad básica, debe escribir muchas repeticiones, porque necesita
Y, sin embargo, esos cinco métodos son básicamente el mismo código, más o menos. En contraste, en Haskell, necesitaría:
Tenga en cuenta que esto no va a cambiar con Java 8 (solo que uno puede aplicar funciones más fácilmente, pero luego, exactamente, se materializará el problema anterior. Mientras no tenga funciones de orden superior, lo más probable es que ni siquiera capaz de comprender para qué son buenos los tipos de clase superior).
Incluso los nuevos lenguajes OO como Ceilán no tienen tipos de tipo superior. (Le pregunté a Gavin King últimamente, y él me dijo que no era importante en este momento). Sin embargo, no sé sobre Kotlin.
*) Para ser justos, puede tener una interfaz Functor que tenga un método fmap. Lo malo es que no puede decir: Hola, sé cómo implementar fmap para la clase de biblioteca SuperConcurrentBlockedDoublyLinkedDequeHasMap, querido compilador, acepte que de ahora en adelante, todos los SuperConcurrentBlockedDoublyLinkedDequeHasMaps son Functores.
fuente
Cualquiera que haya programado en dBase sabría cuán útiles fueron las macros de una sola línea para hacer código reutilizable. Aunque no he programado en Lisp, he leído de muchos otros que juran por compilar macros de tiempo. La idea de inyectar código en su código en tiempo de compilación se utiliza de forma simple en cada programa C con la directiva "include". Debido a que Lisp puede hacer esto con un programa de Lisp y porque Lisp es altamente reflexivo, obtienes inclusiones mucho más flexibles.
Cualquier programador que simplemente tome una cadena de texto arbitraria de la web y la pase a su base de datos no es un programador. Del mismo modo, cualquiera que permita que los datos del "usuario" se conviertan automáticamente en código ejecutable es obviamente estúpido. Eso no significa que permitir que los programas manipulen datos en tiempo de ejecución y luego ejecutarlos como código es una mala idea. Creo que esta técnica será indispensable en el futuro, ya que tendrá un código "inteligente" que realmente escribirá la mayoría de los programas. Todo el "problema de datos / código" o no es una cuestión de seguridad en el idioma.
Uno de los problemas con la mayoría de los idiomas es que fueron creados para que una sola persona fuera de línea ejecute algunas funciones por sí mismo. Los programas del mundo real requieren que muchas personas tengan acceso en todo momento y al mismo tiempo desde múltiples núcleos y múltiples grupos de computadoras. La seguridad debería ser parte del lenguaje más que del sistema operativo y en un futuro no muy lejano lo será.
fuente