¿Cómo me ayuda la programación funcional a razonar sobre mi código (mejor que OOP)? ¿Convertimos las especificaciones en fórmulas lógicas de orden superior y las ejecutamos a través de solucionadores automáticos?

La programación funcional del mundo real no es apátrida.

Es una combinación de aversión al estado y consciente del estado. Obviamente, debe “hacer E / S” (es decir, tener algún efecto en el mundo) para que el objetivo no sea eliminar los efectos con estado. Es mantener el mayor seguimiento posible de ellos. Puede ir tan lejos como para enhebrar explícitamente el estado en una función como parámetro (por ejemplo, la mónada State de Haskell, que modela el estado con un enhebrado explícito, lo que significa que la programación sin estado puede “parecer” con estado para facilitar la lectura). A veces, sin embargo, necesita un estado mutable y, como optimización del rendimiento, está bien. En pequeñas dosis y bajo condiciones controladas, el estado mutable no es malo y no arruinará todo.

La programación funcional se trata de preferir objetos simples que se componen bien: datos inmutables y funciones referencialmente transparentes . Los datos inmutables son más fáciles de construir que un objeto con estado que puede requerir alguna secuencia de inicialización que existe en otra parte del código.

Los efectos con estado, sin control, no componen bien. El punto muerto es un ejemplo especialmente atroz: puede tener dos programas que funcionan perfectamente bien de forma aislada pero que se destruyen entre sí en paralelo. Eso es malo.

La programación imperativa también permite métodos demasiado largos y objetos complicados. Una función con 14 parámetros es una monstruosidad, pero un método Java típico tiene suficientes efectos de estado que, si se contabilizaran explícitamente, y se cubrieran todas las variaciones en la ejecución, ese método se vería bastante atroz en un lenguaje FP, y eso es algo bueno, porque evita que las personas escriban esos métodos de 1000 líneas. Hay buenos usos del estado, pero un mal uso común del estado es externalizar los costos en otras partes del programa: hacer algo horriblemente complicado pero ocultarlo del código principal, generando así el código de espagueti OOP-y.

La programación funcional no es una panacea, y ni siquiera creo que sea un término bien definido, sino que es una cultura de hacer las cosas de manera rápida pero correcta. No es antiestatal. Es un estado anti-inesperado.

El código funcional deja en claro dónde y cuándo se realiza el trabajo, donde el código orientado a objetos puede ocultarlo.

Las funciones tienen interfaces bien articuladas. Ponga algo aquí (a través de los argumentos) y obtenga algo aquí (a través del valor de retorno). En la medida de lo posible, los programadores funcionales buscan mantener este nivel de visibilidad.

Cuando una función es completamente visible, se dice que es pura. Cuando no lo es, se dice que es impuro. Es impuro porque permite que algo suceda en una esquina oscura en lugar de a lo largo de los límites de entrada / salida. Una función que interfiere con las entrañas de los objetos que se le pasan es menos predecible.

Una función ideal no altera las cosas, sino que las reemplaza. Por lo tanto, una función de “ataque” no cambiaría los puntos de golpe asociados con un orco, sino que devolvería una nueva estructura de orco con potencialmente menos puntos de golpe. La magia está tomando el nuevo orco y en el último momento posible permitiendo que se convierta en la representación actual en la próxima iteración del programa. Lograr eso es una preocupación separada. El principio clave es que, en la iteración actual del programa, cada función recibe alguna entrada y emite algo de salida y no sucede nada más en los bordes. En cada paso, lo único que tenemos son entradas, transformaciones y salidas.

Esto hace que probar cada función sea tan simple como podría ser positivamente. Y si una función es excesivamente compleja, uno podría factorizar varias otras funciones puras y usarlas en conjunto para definir esa función. Tal refactorización podría hacerse con gran confianza porque los sucesos son completamente visibles a lo largo de los límites. Esta previsibilidad es lo que hace que razonar sobre el código sea tan fácil. La parte desordenada de efectuar el cambio de estado se realiza de manera controlada en áreas específicas del programa.

Cuando los efectos secundarios se producen al azar, no se puede razonar o refactorizar el código con el mismo nivel de confianza y previsibilidad. Hay un número potencialmente infinito de permutaciones a considerar. Recuerde, los objetos tienen un estado interno. Cuando llama a un método en un objeto, puede mutar ese estado. Las cosas no son terribles a este nivel. El problema se exacerba cuando nos damos cuenta de que algunos objetos contienen referencias a objetos secundarios y / o primarios. En este nivel de acoplamiento, el programador tiene que comprender todas las ramificaciones de lo que podría suceder y dónde al llamar a algún método. Lo que tenemos es un problema de límites (Ley de Deméter). Cuando usamos objetos, nuestros límites son vagos y las posibilidades son significativamente más difíciles de comprender y predecir.

Con funciones puras, los límites son absolutos y las posibilidades están bien limitadas a los sucesos de la función misma. Ya no tenemos que mirar a los bordes y preguntarnos dónde podría ocurrir un cambio. Sabemos exactamente dónde se produce el cambio y podemos probarlo con total confianza. Con límites absolutos, tenemos la capacidad de razonar sobre el código tan buena como sea posible. Esto permite diseñar la mayor parte del programa en condiciones ideales.

Puede escribir 1 y 2 en OOP pero puede verse fácilmente comprometido. Con FP, no es posible comprometerlos (tiene que usar los bloques begin y commit para las declaraciones de asignación) sin ser detectado.

El número 3 se trata de garantías. En Haskell, hay una distinción entre una función con efectos secundarios (aquellos con tipos de datos ) y funciones sin efectos secundarios. Entonces, llamas a las funciones peligrosas (con efectos secundarios) de manera diferente a las funciones seguras. Probar funciones seguras es rápido y memorable (no es un error tipográfico).
Ejemplo:
Función peligrosa: SaveObject
Funciones seguras: ValidateX, ValidateY, CalculateZ

Usted prueba:
ValidateX
entrada – salida – esperada
[1,3,4] – verdadero – verdadero
[1,2,4] – verdadero – falso

SaveObject
–Estado de prueba
Trans.Begin ();
SaveObject ();
– Estado de prueba;
Trans.Rollback ();

La prueba de las funciones seguras se realiza mediante tablas de entrada / salida, mientras que esas funciones peligrosas se prueban SOLO con efectos secundarios. En Java, la construcción de lanzamientos permite esta distinción, pero la gente los comería en el bloque try-catch con bastante facilidad.

Esto se llama honestidad funcional : la honestidad de la función de sus dependencias y su estado.

Entonces, para todas las funciones seguras a (b (x), b (x)), el compilador tiene la libertad de usar una llamada (con resultado reciclado) para b (x) porque garantiza que el bloque no tiene efectos secundarios . También puede capturar la llamada a a () y probarla por separado.

En breve:
1.) El razonamiento es mucho más fácil con un control de estado intransigente (en oposición al control de estado comprometido en la POO).
2.) El razonamiento es mucho más fácil si puede probar los efectos secundarios de los efectos secundarios mientras que el resto de las funciones sin efectos secundarios se prueban en el estilo esperado frente al real.
3.) El razonamiento es mucho más fácil si puede suponer que las llamadas a funciones no cambian en su salida. Esta suposición debe ser compiladora, lo que hace que la pila de llamadas sea más fácil de entender.

Para mí, eso se deduce de su 1 y 2.

Sí, puede escribir código sin estado que use estructuras de datos inmutables en lenguajes no funcionales, pero es más engorroso hacerlo, es probable que el compilador no lo optimice bien y, quizás lo más importante, no sea verificable por el compilador. Puede esforzarse por hacer esto, pero debido a que se aplica solo por la disciplina del programador, inevitablemente se desviará debido a la pereza, las concesiones prácticas o incluso los simples errores, por no hablar de la voluntad y la capacidad de sus compañeros de trabajo de seguir esa disciplina, o de lo necesario uso de bibliotecas de terceros que no lo convierten en un objetivo.

En la práctica, generalmente es más engorroso usar un estilo funcional / declarativo en un lenguaje que no está diseñado para ese estilo. Tendría que asignar manualmente nuevas estructuras y copiar laboriosamente los valores, lo que puede ser un verdadero problema para las estructuras de datos. Sería un caso clásico de lucha con el lenguaje.

Cualquier proyecto significativamente complejo se verá afectado por la incertidumbre de que el diseño en realidad no proporciona los beneficios que ha enumerado cuando trabaja con un lenguaje que no prueba meticulosamente que se cumplan esas restricciones funcionales. Cuando su idioma no puede proporcionar estas garantías, es una tontería esperar que disfrute de los beneficios de un diseño funcional / declarativo. La naturaleza de los lenguajes funcionales que los hace más fáciles de razonar proviene principalmente de la previsibilidad, y cuando su lenguaje es impotente para garantizar esa previsibilidad, no puede contar con que ese razonamiento sea válido o aplicable a su código.

Convertirse incluso en un programador funcional mediocre requiere que aprendas una habilidad particular, estructurar bien las expresiones, que otros programadores aprenden solo por casualidad, o no lo hacen en absoluto.

Otros han hablado sobre el impacto directo de las características de FP (como transparencia referencial) y modismos (como funciones de composición) en el trabajo del programador; Esta respuesta será específicamente sobre la educación de un programador.

El componente principal de los programas funcionales es la expresión , sí, la función también, pero sigue leyendo, mientras que los programas imperativos dependen más de la declaración . Esto parecerá trivial para los buenos programadores, pero:

  • Las expresiones, como las funciones, se combinan naturalmente (ya que se definen de forma recursiva para contener otras expresiones).
  • El hecho de que las expresiones están asociadas con los tipos , incluso en lenguajes de tipo dinámico; puede que no estén escritos en el código, pero un programador decente estará al tanto de los tipos en su modelo mental del programa, y ​​uno bueno los comentará donde no sean obvios: puede guiar al programador a combinar expresiones, en una manera que no tiene un análogo común en el mundo imperativo (de secuencias de enunciados).
  • Existen expresiones en casi todos los idiomas modernos que ven un uso común, pero …
  • En lenguajes que fomentan la programación imperativa, el programador tiene relativamente poca práctica para construir expresiones no triviales, mientras que en lenguajes funcionales, construir expresiones sustanciales (y aprender a manejar su complejidad) es casi inevitable.

En consecuencia, si toda su experiencia en programación es imprescindible, probablemente le falte una herramienta clave para administrar la complejidad de sus programas.

Debido a que las secuencias de declaraciones son más parecidas al lenguaje que usan los no programadores para describir los procedimientos, los programadores novatos perciben el pensamiento imperativo como más simple . En cierto modo, esa es una percepción precisa: imperativamente puede ser la forma más directa de resolver problemas muy simples , pero es engañoso, ya que ese lenguaje también es la razón por la que la mayoría de las personas tienen dificultades con el pensamiento algorítmico: cada declaración está cargada de suposiciones y tácitos. conocimiento sobre lo que sucedió antes y lo que sucederá después. Esta es la programación en su forma más cruda, básicamente común, si no estándar, tarifa educativa K-12, y es necesaria pero no lo suficiente para armar un programa complejo con la seguridad de que funcionará. Parece que funciona parece razonable para el principiante, pero con la experiencia uno aprende que parece que no es lo suficientemente bueno para el código que otras personas usarán.

Entonces, la programación funcional es una parte importante de la educación de un programador porque le enseña cómo construir expresiones .

Para muchos niños de trece años, escribir un ensayo es un gran problema. Para algunos que están detrás de la curva, escribir un párrafo es un gran problema. Tienen un camino por recorrer antes de aprender a estructurar un párrafo o un ensayo para beneficio del lector, y aún más antes de que puedan hacerlo de manera concisa; aún no han superado la intimidación de tener que juntar tantas palabras escritas de manera coherente.

Para muchos programadores, especialmente los estudiantes, escribir una gran expresión es un gran problema. Tienen un largo camino por recorrer antes de que puedan aprender el arte de factorizar expresiones en componentes cuya estructura hace obvio el significado y la operación del programa. (¡Y, además, esa misma habilidad está en el corazón de poder crear unidades significativas de código imperativo también!) Sin embargo, escribir una gran expresión es a veces la forma más clara de expresar algo.

Aprenda FP para ser un programador para quien escribir una gran expresión no es gran cosa.

No le ayuda a usted (ni a ningún programador dado) a razonar sobre la semántica real de su código, lo hace posible de una manera mucho más confiable y manejable.

Las funciones también componen mucho mejor que los objetos. Con los objetos, hay una conexión programada entre instancias de dos o más clases o se debe escribir algún código para unirlos. Con las funciones, simplemente las “conecta” (las compone) o asigna una función a otra función de “orden superior” y trabajan juntas utilizando los mecanismos inherentes del lenguaje funcional.

Mi analogía es que los objetos son como canicas. Simplemente se sientan allí y rebotan entre sí. Las funciones son como plomería: conéctelas y los datos fluirán a través de ellas por sí mismas.

Un par de formas diferentes. Primero, si tiene un código como (usando la sintaxis de Java)

public int doSomething (SomeObject x) {
devuelve firstThing (x) + secondThing (x);
}

Una cosa que será importante entender es lo que su método doSomething devuelve para diferentes entradas. Si sabe qué devuelven firstThing y secondThing, podría pensar que el problema es fácil: doSomething simplemente devuelve la suma de los dos. Para el programa funcional equivalente, sería así de fácil. Desafortunadamente, firstThing podría modificar x, en cuyo caso sus expectativas sobre doSomething estarían mal. De hecho, eso significaría que firstThing(x) + secondThing(x) y secondThing(x) + firstThing(x) podrían dar resultados diferentes. Demasiado para que la suma sea conmutativa.

La segunda forma en que un lenguaje OO hace que el código sea más difícil de razonar que un lenguaje funcional es a través de variables estáticas. Por ejemplo, si tienes un código como

public int myMethod () {
int x = otherMethod ();
if (x% 2 == 0) return otherMethod ();
devuelve 0;
}

Obviamente, es una tontería llamar a otherMethod dos veces; este código debe ser equivalente:

public int myMethod () {
int x = otherMethod ();
if (x% 2 == 0) devuelve x;
devuelve 0;
}

En un lenguaje funcional sería equivalente. Pero no en un idioma OO. Considere esta implementación de otherMethod:

privado estático int ctr = 0;
public static int otherMethod () {
return ++ ctr;
}

Cada vez que llama a otherMethod, obtiene un resultado diferente, por lo que, aunque parece perfectamente inocente reemplazar una llamada a otherMethod () por el resultado guardado de la llamada anterior, cambiar el comportamiento del código por completo.

Mire los fragmentos de código para realizar tareas específicas en Rosetta Code
Observe qué poco código hay en los lenguajes funcionales (no necesariamente Scala o Clojure). Python también es muy expresivo y cada vez más. Algunas personas se burlan del azúcar sintáctico, pero hace que la codificación, pensar en codificar y leer el código sea mucho más fácil. La perspicacia deriva de una lógica aguda. Trabajar con cualquier cosa tan cercana a las funciones puras es mucho más fácil que toda la sobrecarga requerida por lenguajes especialmente orientados a objetos. Los lenguajes funcionales hacen que la complejidad sea manejable. Los lenguajes orientados a objetos intentan ocultarlo.

Al razonar en un programa funcional, se ve obligado a separar su sistema en cómputo sin estado y el estado crítico necesario para que su aplicación se mantenga. El pensamiento OOP tiende a hacer que cada invocación de un método toque algún estado privado. La noción de estado privado no es la mejor manera de construir sistemas distribuidos escalables.

More Interesting

'80% de los recursos de desarrollo de software se destinan a pruebas (QA) '. ¿Es esta la verdad o un mito?

¿Qué tipo de KPI podría establecer para el equipo Scrum?

¿Cuáles son algunos lenguajes / métodos y herramientas de programación modernos utilizados por las nuevas empresas de Internet de hoy?

¿Dónde puedo encontrar un buen programador / desarrollador europeo por menos de 10 USD / hora?

¿Es cierto que los buenos programadores no depuran?

¿Por qué debería usar o no usar UML?

Tengo 7 años de experiencia en desarrollo de software. ¿Es mejor cambiar mi carrera hacia la ciencia de datos / aprendizaje automático ahora?

Si pudiera volver a implementar la World Wide Web desde cero, incluidas todas las tecnologías y protocolos relevantes, ¿qué haría de manera diferente?

Para los ingenieros de software, ¿es productivo su entorno de desarrollador en el trabajo?

¿Has asistido a un campamento de desarrolladores de software en el último año? ¿Por qué estás extasiado o furioso por los resultados que logró para ti?

¿Cómo se determina quién posee el código fuente?

¿En qué se diferencia la maestría en ciencias de la computación en Oxford de una maestría de una de las principales instituciones de EE. UU. Como Stanford, la CMU o Berkeley? ¿Tiene buena reputación en Silicon Valley? Si no, ¿por qué?

¿Ves un énfasis excesivo en la programación competitiva entre los coroanos?

¿Cómo te sientes cuando trabajas en más de 500 mil líneas de código?

¿Qué es la prueba de caja negra en sí?