¿Qué características funcionales valen un poco la confusión de OOP por los beneficios que aportan?

13

Después de aprender la programación funcional en Haskell y F #, el paradigma de OOP parece retroceder con clases, interfaces, objetos. ¿Qué aspectos de FP puedo llevar al trabajo que mis compañeros de trabajo puedan entender? ¿Vale la pena hablar con mi jefe sobre los estilos de FP para volver a capacitar a mi equipo para que podamos usarlos?

Posibles aspectos de la PF:

  • Inmutabilidad
  • Aplicación parcial y curry
  • Funciones de primera clase (punteros de función / objetos funcionales / patrón de estrategia)
  • Evaluación perezosa (y mónadas)
  • Funciones puras (sin efectos secundarios)
  • Expresiones (frente a declaraciones: cada línea de código produce un valor en lugar de, o además de causar efectos secundarios)
  • Recursividad
  • La coincidencia de patrones

¿Es un programa gratuito para todos donde podemos hacer lo que sea que el lenguaje de programación admite hasta el límite que ese lenguaje lo admite? ¿O hay una mejor pauta?

Trident D'Gao
fuente
66
Yo tuve una experiencia similar. Después de aproximadamente 2 meses de dolor, comencé a encontrar un buen balance de "cosas que se asignan a objetos" y "cosas que se asignan a funciones". Ayuda a hacer algún hackeo serio en un lenguaje que sea compatible con ambos. Al final, mis habilidades de FP y OOP han mejorado enormemente
Daniel Gratzer
3
FWIW, Linq es funcional y vago, y puede simular la programación funcional en C # utilizando métodos estáticos y evitando la persistencia del estado.
Robert Harvey el
1
En este punto, deberías leer sicp . Es gratis y está bien escrito. Ofrece una buena comparación entre los dos paradigmas.
Simon Bergot el
44
FP y OOP son al mismo tiempo en cierto sentido ortogonales y en cierto sentido duales. OOP trata sobre la abstracción de datos, FP trata sobre (la ausencia de) efectos secundarios. Si tiene o no efectos secundarios es ortogonal a la forma en que abstrae sus datos. Lambda Calculus es funcional y orientado a objetos, por ejemplo. Sí, FP generalmente usa tipos de datos abstractos, no objetos, pero también podría usar objetos en su lugar sin ser menos FP. OTOH, también hay una relación profunda: una función es isomórfica a un objeto con un solo método (así es como son "falsificados" en Java, e implementados en Java8, ...
Jörg W Mittag
3
Creo que el aspecto más fuerte de su pregunta tiene que ver con la legibilidad. "¿Cuánto estilo de programación funcional es apropiado para llevar a trabajar en una tienda orientada a objetos?" O qué características funcionales valen un poco de confusión de OOP por los beneficios que aportan.
GlenPeterson

Respuestas:

13

La programación funcional es un paradigma diferente de la programación orientada a objetos (una mentalidad diferente y una forma diferente de pensar acerca de los programas). Has comenzado a darte cuenta de que aquí hay más de una forma (orientada a objetos) para pensar sobre los problemas y sus soluciones. Hay otros (viene a la mente la programación procesal y genérica). La forma en que reaccione a este nuevo conocimiento, si acepta e integra estas nuevas herramientas y enfoques en su conjunto de habilidades, determinará si crece y se convierte en un desarrollador más completo y calificado.

Todos estamos capacitados para manejar y nos sentimos cómodos con un cierto nivel de complejidad. Me gusta llamar a esto el límite de cabello de una persona (desde Watership Down, qué tan alto puedes contar). Es una gran cosa expandir su mente, su capacidad de considerar más opciones y tener más herramientas para abordar y resolver problemas. Pero es un cambio y te saca de tu zona de confort.

Un problema que puede encontrar es que estará menos contento de seguir a la multitud "todo es un objeto". Es posible que tenga que desarrollar paciencia mientras trabaja con personas que tal vez no entiendan (o quieran entender) por qué un enfoque funcional para el desarrollo de software funciona bien para ciertos problemas. Así como un enfoque de programación genérico funciona bien para ciertos problemas.

¡Buena suerte!

ChuckCottrill
fuente
3
Me gustaría agregar que se puede obtener una mejor comprensión de algunos conceptos tradicionales de OOP cuando se trabaja con lenguajes funcionales como Haskell o Clojure. Personalmente, me di cuenta de que el polimorfismo es realmente un concepto importante (Interfaces en Java o clases de tipos en Haskell), mientras que la herencia (lo que pensé que era un concepto definitorio) es una especie de abstracción extraña.
wirrbel
6

La programación funcional produce una productividad muy práctica y práctica en la escritura de códigos de todos los días: algunas características favorecen la concisión, lo cual es excelente porque cuanto menos código escriba, menos fallas tendrá y se requerirá menos mantenimiento.

Siendo matemático, encuentro cosas funcionales sofisticadas muy atractivas, pero generalmente es útil al diseñar una aplicación: estas estructuras pueden codificar en la estructura del programa muchos invariantes del programa, sin representar estos invariantes por variables.

Mi combinación favorita puede parecer bastante trivial, sin embargo, creo que tiene un impacto muy alto en la productividad. Esta combinación es Aplicación parcial y funciones de currificación y primera clase que volvería a etiquetar nunca volvería a escribir un ciclo for : en su lugar, pase el cuerpo del ciclo a una función de iteración o mapeo. Recientemente fui contratado para un trabajo en C ++ y me di cuenta de manera divertida, ¡perdí totalmente el hábito de escribir for-loops!

La combinación de Recursion y Pattern Matching aniquila la necesidad de ese patrón de diseño Visitor . Simplemente compare el código que necesita para programar un evaluador de expresiones booleanas: en cualquier lenguaje de programación funcional, esto debería ser alrededor de 15 líneas de código, en OOP lo correcto es usar ese patrón de diseño Visitor , que convierte ese ejemplo de juguete en Un extenso ensayo. Las ventajas son obvias y no conozco ningún inconveniente.

usuario40989
fuente
2
Estoy completamente de acuerdo, pero he tenido retroceso de personas que en toda la industria tienden a estar de acuerdo: conocen el patrón de visitantes, lo han visto y usado muchas veces, por lo que el código es algo que entienden y con el que están familiarizados. otro enfoque, aunque ridículamente más simple y fácil, es extraño y, por lo tanto, más difícil para ellos. Este es un hecho desafortunado de que la industria haya tenido más de 15 años de OOP en cada cabeza de programador de que más de 100 líneas de código son más fáciles de entender que 10 simplemente porque memorizaron más de 100 líneas después de repetirlas durante una década.
Jimmy Hoffa
1
-1 - Más código conciso no significa que está escribiendo código "menos". Estás escribiendo el mismo código con menos caracteres. En todo caso, comete más errores porque el código es (a menudo) más difícil de leer.
Telastyn
8
@Telastyn: Terse no es lo mismo que ilegible. Además, las grandes masas de placas hinchadas tienen su propia forma de ser ilegibles.
Michael Shaw
1
@Telastyn Creo que acabas de tocar el quid real aquí, sí, por supuesto, puede ser malo e ilegible, hinchado también puede ser malo e ilegible, pero la clave no es la longitud variable y el código escrito de manera extraña. La clave es como se menciona más arriba número de operaciones, no estoy de acuerdo que el número de operaciones no se correlaciona con la mantenibilidad, creo que haciendo menos cosas (con el código escrito con claridad) hace legibilidad beneficio y facilidad de mantenimiento. Obviamente, hacer el mismo número de cosas con la función de una sola letra y los nombres de las variables no ayudará, un buen FP necesita considerablemente menos operaciones aún claramente escritas
Jimmy Hoffa
2
@ user949300: si desea un ejemplo completamente diferente, ¿qué tal este ejemplo de Java 8? list.forEach(System.out::println);Desde el punto de vista de FP, printlnes una función que toma dos argumentos, un objetivo PrintStreamy un valor, Objectpero el método Collection's forEachespera una función con un solo argumento que se puede aplicar a cada elemento. Entonces, el primer argumento está vinculado a la instancia que se encuentra al System.outceder el paso a una nueva función con un argumento. Es más simple queBiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));
Holger
5

Es posible que tenga que restringir qué partes de su conocimiento usa en el trabajo, la forma en que Superman tiene que fingir que es Clark Kent para disfrutar de las ventajas de una vida normal. Pero saber más nunca te hará daño. Dicho esto, algunos aspectos de la programación funcional son apropiados para una tienda orientada a objetos, y vale la pena hablar con su jefe sobre otros aspectos para que pueda aumentar el nivel de conocimiento promedio de su tienda y, como resultado, escribir un mejor código.

FP y OOP no son mutuamente excluyentes. Mira a Scala. Algunos piensan que es lo peor porque es FP impuro, pero algunos piensan que es lo mejor por esa misma razón.

Uno por uno, aquí hay algunos aspectos que funcionan muy bien con OOP:

  • Funciones puras (sin efectos secundarios): todos los lenguajes de programación que conozco admiten esto. Hacen que su código sea mucho más fácil de razonar y deben usarse siempre que sea práctico. No tienes que llamarlo FP. Solo llámalo buenas prácticas de codificación.

  • Inmutabilidad: String es posiblemente el objeto Java más utilizado y es inmutable. Cubro objetos inmutables Java y inmutables colecciones de Java en mi blog. Algo de eso puede ser aplicable a usted.

  • Funciones de primera clase (punteros de función / Objetos funcionales / Patrón de estrategia): Java ha tenido una versión mutante y cobarde de esto desde la versión 1.1 con la mayoría de las clases de API (y hay cientos) que implementan la interfaz de escucha. Runnable es probablemente el objeto funcional más utilizado. Las funciones de primera clase requieren más trabajo para codificar en un lenguaje que no las admite de forma nativa, pero a veces vale la pena el esfuerzo adicional cuando simplifican otros aspectos de su código.

  • La recursión es útil para procesar árboles. En una tienda OOP, ese es probablemente el uso principal apropiado de la recursividad. El uso de la recursión por diversión en OOP probablemente debería estar mal visto si, por ninguna otra razón, la mayoría de los lenguajes OOP no tienen el espacio de pila por defecto para hacer de esto una buena idea.

  • Expresiones (frente a declaraciones: cada línea de código produce un valor en lugar de, o además de causar efectos secundarios): el único operador evaluativo en C, C ++ y Java es el operador ternario . Discuto el uso apropiado en mi blog. Puede encontrar que escribe algunas funciones simples que son altamente reutilizables y evaluativas.

  • Evaluación diferida (y mónadas): principalmente restringida a la inicialización diferida en OOP. Sin funciones de lenguaje que lo admitan, puede encontrar algunas API que son útiles, pero escribir las suyas es difícil. En su lugar, maximice el uso de las transmisiones: consulte las interfaces de Writer y Reader para ver ejemplos.

  • Aplicación parcial y currículum - No es práctico sin funciones de primera clase.

  • Coincidencia de patrones: generalmente desaconsejado en OOP.

En resumen, no creo que el trabajo deba ser libre para todos, en el que puedas hacer lo que sea que el lenguaje de programación admita hasta el límite que lo admita el lenguaje. Creo que la legibilidad de tus compañeros de trabajo debería ser tu prueba de fuego para el código hecho para contratar. Donde eso te irrita más, buscaría comenzar un poco de educación en el trabajo para ampliar los horizontes de tus compañeros de trabajo.

GlenPeterson
fuente
Desde que aprendí FP me he acostumbrado a diseñar cosas para tener interfaces fluidas que resulten en lo que es similar a las expresiones, una función que tiene una declaración que hace un montón de cosas. Es lo más cercano que realmente obtendrá, pero es un enfoque que fluye naturalmente de la pureza cuando descubre que ya no tiene ningún método nulo, el uso de métodos de extensión estática en C # ayuda en gran medida. De esa manera, su punto de expresión es el único punto con el que no estoy de acuerdo, todo lo demás es acertado con mis propias experiencias aprendiendo FP y trabajando en un trabajo diario .NET
Jimmy Hoffa
Lo que realmente me molesta en C # ahora es que no puedo usar delegados en lugar de interfaces de un método por 2 razones simples: 1. No puedes hacer lambas recursivas sin un hack (asignando a null primero y luego a un lambda segundo) o un Y- combinador (que es tan feo como el infierno en C #). 2. no hay alias de tipo que pueda usar en el alcance de un proyecto, por lo que las firmas de sus delegados se vuelven rápidamente inmanejables. Entonces, solo por estas 2 estúpidas razones, ya no puedo disfrutar de C #, porque lo único que puedo hacer que funcione es mediante el uso de interfaces de un método, que es un trabajo adicional innecesario.
Trident D'Gao
@bonomo Java 8 tiene una función genérica java.util.function.BiConsumer que podría ser útil en C #: public interface BiConsumer<T, U> { public void accept(T t, U u); }hay otras interfaces funcionales útiles en java.util.function.
GlenPeterson
@bonomo Hola, lo entiendo, este es el dolor de Haskell. Cada vez que lees a alguien que dice "Aprender FP me hizo mejor en OOP" significa que aprendió Ruby o algo que no es puro y declarativo como Haskell. Haskell deja en claro que OOP es inútilmente bajo nivel. El mayor dolor con el que te encuentras es que la inferencia de tipos basada en restricciones no se puede decidir cuando no estás en el sistema de tipos HM, por lo que la inferencia de tipos basada en cosntraint no se realiza por completo: blogs.msdn.com/b/ericlippert/archive / 2012/03/09 /…
Jimmy Hoffa
1
Most OOP languages don't have the stack space for it De Verdad? Todo lo que necesitaría es 30 niveles de recursión para administrar miles de millones de nodos en un árbol binario equilibrado. Estoy bastante seguro de que mi espacio de pila es adecuado para muchos más niveles que este.
Robert Harvey
3

Además de la programación funcional y la programación orientada a objetos, también hay programación declarativa (SQL, XQuery). Aprender cada estilo te ayuda a obtener nuevos conocimientos, y aprenderás a elegir la herramienta adecuada para el trabajo.

Pero sí, puede ser muy frustrante escribir código en un idioma, y ​​saber que si estuviera usando otra cosa, podría ser mucho más productivo para un dominio de problema en particular. Sin embargo, incluso si está utilizando un lenguaje como Java, es posible aplicar conceptos de FP a su código Java, aunque de manera indirecta. El marco de la guayaba, por ejemplo, hace algo de esto.

sgwizdak
fuente
2

Como programador, creo que nunca debes dejar de aprender. Dicho esto, es muy interesante que aprender FP esté contaminando tus habilidades de OOP. Tiendo a pensar en aprender OOP como aprender a andar en bicicleta; nunca olvidas cómo hacerlo.

A medida que aprendí los entresijos de FP, me encontré pensando más matemáticamente y obtuve una mejor perspectiva de los medios en los que escribo el software. Esa es mi experiencia personal.

A medida que gane más experiencia, los conceptos básicos de programación serán mucho más difíciles de perder. Por lo tanto, le sugiero que lo tome con calma en el FP hasta que los conceptos de POO se solidifiquen totalmente en su mente. FP es un cambio de paradigma definitivo. ¡Buena suerte!

Bobby Gammill
fuente
44
Aprender OOP es como aprender a gatear. Pero una vez que te levantas constantemente, solo recurrirás a gatear cuando estés demasiado borracho. Por supuesto, no puede olvidar cómo hacerlo, pero normalmente no querría hacerlo. Y será una experiencia dolorosa caminar con los rastreadores cuando sepas que puedes correr.
SK-logic
@ SK-logic, me gusta tu metáfora
Trident D'Gao
@ SK-Logic: ¿Cómo es el aprendizaje de la programación imperativa? ¿Arrastrándote sobre tu estómago?
Robert Harvey
@RobertHarvey intenta enterrarse bajo tierra con una cuchara oxidada y una baraja de tarjetas perforadas.
Jimmy Hoffa
0

Ya hay muchas buenas respuestas, por lo que la mía abordará un subconjunto de su pregunta; a saber, considero la premisa de su pregunta, ya que las características funcionales y OOP no son mutuamente excluyentes.

Si usa C ++ 11, hay muchas de estas características de programación funcional integradas en el lenguaje / biblioteca estándar que sinergizan (bastante) bien con OOP. Por supuesto, no estoy seguro de qué tan bien TMP será recibido por su jefe o compañeros de trabajo, pero el punto es que puede obtener muchas de estas características de una forma u otra en lenguajes no funcionales / OOP, como C ++.

El uso de plantillas con recursión en tiempo de compilación depende de sus primeros 3 puntos,

  • Inmutabilidad
  • Recursividad
  • La coincidencia de patrones

En que los valores de plantilla son inmutables (constantes de tiempo de compilación), cualquier iteración se realiza mediante recursión, y la ramificación se realiza utilizando (más o menos) coincidencia de patrones, en forma de resolución de sobrecarga.

En cuanto a los otros puntos, el uso de std::bindy std::functionle brinda una aplicación de función parcial, y los punteros de función están integrados en el lenguaje. Los objetos invocables son objetos funcionales (así como una aplicación de función parcial). Tenga en cuenta que por objetos invocables, me refiero a los que definen sus operator ().

La evaluación perezosa y las funciones puras serían un poco más difíciles; para funciones puras, puede usar funciones lambda que solo capturan por valor, pero esto no es ideal.

Por último, aquí hay un ejemplo del uso de la recursión en tiempo de compilación con la aplicación de función parcial. Es un ejemplo un tanto artificial, pero demuestra la mayoría de los puntos anteriores. Enlazará recursivamente los valores en una tupla dada a una función dada y generará un objeto de función (invocable)

#include <iostream>
#include <functional>

//holds a compile-time index sequence
template<std::size_t ... >
struct index_seq
{};

//builds the index_seq<...> struct with the indices (boils down to compile-time indexing)
template<std::size_t N, std::size_t ... Seq>
struct gen_indices
  : gen_indices<N-1, N-1, Seq ... >
{};

template<std::size_t ... Seq>
struct gen_indices<0, Seq ... >
{
    typedef index_seq<Seq ... > type;
};


template <typename RType>
struct bind_to_fcn
{
    template <class Fcn, class ... Args>
    std::function<RType()> fcn_bind(Fcn fcn, std::tuple<Args...> params)
    {
        return bindFunc(typename gen_indices<sizeof...(Args)>::type(), fcn, params);
    }

    template<std::size_t ... Seq, class Fcn, class ... Args>
    std::function<RType()> bindFunc(index_seq<Seq...>, Fcn fcn, std::tuple<Args...> params)
    {
        return std::bind(fcn, std::get<Seq>(params) ...);
    }
};

//some arbitrary testing function to use
double foo(int x, float y, double z)
{
    return x + y + z;
}

int main(void)
{
    //some tuple of parameters to use in the function call
    std::tuple<int, float, double> t = std::make_tuple(1, 2.04, 0.1);                                                                                                                                                                                                      
    typedef double(*SumFcn)(int,float,double);

    bind_to_fcn<double> binder;
    auto other_fcn_obj = binder.fcn_bind<SumFcn>(foo, t);
    std::cout << other_fcn_obj() << std::endl;
}
alrikai
fuente