Programación funcional en comparación con OOP con clases

32

Últimamente me han interesado algunos de los conceptos de programación funcional. He usado OOP desde hace algún tiempo. Puedo ver cómo construiría una aplicación bastante compleja en OOP. Cada objeto sabría cómo hacer las cosas que hace ese objeto. O cualquier cosa que haga la clase de padres también. Entonces puedo decirle simplemente que hable Person().speak()a la persona.

Pero, ¿cómo hago cosas similares en la programación funcional? Veo cómo las funciones son elementos de primera clase. Pero esa función solo hace una cosa específica. ¿Simplemente tendría un say()método flotando y lo llamaría con un equivalente de Person()argumento para saber qué tipo de cosa está diciendo algo?

Entonces puedo ver las cosas simples, ¿cómo haría lo comparable de OOP y los objetos en la programación funcional, para poder modularizar y organizar mi base de código?

Como referencia, mi experiencia principal con OOP es Python, PHP y algunos C #. Los idiomas que estoy viendo que tienen características funcionales son Scala y Haskell. Aunque me estoy inclinando hacia Scala.

Ejemplo básico (Python):

Animal(object):
    def say(self, what):
        print(what)

Dog(Animal):
    def say(self, what):
        super().say('dog barks: {0}'.format(what))

Cat(Animal):
    def say(self, what):
        super().say('cat meows: {0}'.format(what))

dog = Dog()
cat = Cat()
dog.say('ruff')
cat.say('purr')
skift
fuente
Scala está diseñado como OOP + FP, por lo que no tiene que elegir
Karthik T
1
Sí, lo sé, pero también quiero saber por razones intelectuales. No puedo encontrar nada sobre el equivalente de objeto en lenguajes funcionales. En cuanto a scala, todavía me gustaría saber cuándo / dónde / cómo debo usar funcional sobre oop, pero esa en mi humilde opinión es otra pregunta.
Skift
2
"Particularmente sobre enfatizado, la OMI es la noción de que no mantenemos el estado": esta es la noción incorrecta. No es cierto que FP no use el estado, sino que FP maneja el estado de una manera diferente (por ejemplo, mónadas en Haskell o tipos únicos en Clean).
Giorgio
1
posible duplicado de Cómo organizar programas funcionales
Doc Brown
3
posible duplicado de Programación Funcional vs. OOP
Caleb

Respuestas:

21

Lo que realmente está preguntando aquí es cómo hacer el polimorfismo en lenguajes funcionales, es decir, cómo crear funciones que se comporten de manera diferente en función de sus argumentos.

Tenga en cuenta que el primer argumento de una función suele ser equivalente al "objeto" en OOP, pero en los lenguajes funcionales generalmente desea separar las funciones de los datos, por lo que es probable que el "objeto" sea un valor de datos puro (inmutable).

Los lenguajes funcionales en general proporcionan varias opciones para lograr el polimorfismo:

  • Algo así como métodos múltiples que llaman a una función diferente basada en el examen de los argumentos proporcionados. Esto se puede hacer en el tipo del primer argumento (que es efectivamente igual al comportamiento de la mayoría de los lenguajes OOP), pero también se puede hacer en otros atributos de los argumentos.
  • Estructuras de datos tipo prototipo / objeto que contienen funciones de primera clase como miembros . Por lo tanto, podría incrustar una función "decir" dentro de las estructuras de datos de su perro y gato. Efectivamente, ha hecho que el código forme parte de los datos.
  • Coincidencia de patrones : donde la lógica de coincidencia de patrones está integrada en la definición de la función y garantiza diferentes comportamientos para diferentes parámetros. Común en Haskell.
  • Ramificación / condiciones : equivalente a las cláusulas if / else en OOP. Puede que no sea muy extensible, pero aún así puede ser apropiado en muchos casos cuando tiene un conjunto limitado de valores posibles (por ejemplo, ¿se pasó la función a un número o una cadena o nulo?)

Como ejemplo, aquí hay una implementación de Clojure de su problema utilizando métodos múltiples:

;; define a multimethod, that dispatched on the ":type" keyword
(defmulti say :type)  

;; define specific methods for each possible value of :type. You can add more later
(defmethod say :cat [animal what] (println (str "Car purrs: " what)))
(defmethod say :dog [animal what] (println (str "Dog barks: " what)))
(defmethod say :default [animal what] (println (str "Unknown noise: " what)))

(say {:type :dog} "ruff")
=> Dog barks: ruff

(say {:type :ape} "ook")
=> Unknown noise: ook

Tenga en cuenta que este comportamiento no requiere que se definan clases explícitas: los mapas regulares funcionan bien. La función de despacho (: tipo en este caso) podría ser cualquier función arbitraria de los argumentos.

mikera
fuente
No está 100% despejado, pero es suficiente para ver a dónde va. Podría ver esto como el código 'animal' en un archivo dado. También la parte de ramificación / condiciones también es buena. No lo había considerado como la alternativa a if / else.
skift
11

Esta no es una respuesta directa, ni es necesariamente 100% precisa, ya que no soy un experto en lenguaje funcional. Pero en cualquier caso, compartiré contigo mi experiencia ...

Hace aproximadamente un año estaba en un barco similar al tuyo. He hecho C ++ y C # y todos mis diseños siempre fueron muy pesados ​​en OOP. Escuché acerca de los lenguajes FP, leí información en línea, hojeé el libro de F #, pero aún no pude comprender cómo un lenguaje FP puede reemplazar a OOP o ser útil en general, ya que la mayoría de los ejemplos que he visto son demasiado simples.

Para mí, el "avance" se produjo cuando decidí aprender Python. Descargué python, luego fui a la página de inicio del proyecto euler y comencé a resolver un problema tras otro. Python no es necesariamente un lenguaje FP y ciertamente puedes crear clases en él, pero en comparación con C ++ / Java / C #, tiene muchas más construcciones FP, así que cuando comencé a jugar con él, tomé una decisión consciente de no hacerlo. definir una clase a menos que sea absolutamente necesario.

Lo que encontré interesante sobre Python fue lo fácil y natural que era tomar funciones y "unirlas" para crear funciones más complejas y al final su problema aún se resolvió llamando a una sola función.

Usted señaló que al codificar debe seguir el principio de responsabilidad única y eso es absolutamente correcto. Pero el hecho de que la función sea responsable de una sola tarea no significa que solo pueda hacer el mínimo absoluto. En FP, todavía tienes niveles de abstracción. Por lo tanto, sus funciones de nivel superior aún pueden hacer "una" cosa, pero pueden delegar a funciones de nivel inferior para implementar detalles más precisos de cómo se logra esa "una" cosa.

Sin embargo, la clave con FP es que no tiene efectos secundarios. Siempre que trate la aplicación como una simple transformación de datos con un conjunto definido de entradas y un conjunto de salidas, puede escribir un código FP que logre lo que necesita. Obviamente, no todas las aplicaciones encajarán bien en este molde, pero una vez que comience a hacerlo, se sorprenderá de cuántas aplicaciones encajan. Y aquí es donde creo que Python, F # o Scala brillan porque te dan construcciones de FP, pero cuando necesitas recordar tu estado e "introducir efectos secundarios", siempre puedes recurrir a técnicas OOP verdaderas y probadas.

Desde entonces, he escrito un montón de código de Python como utilidades y otros scripts de ayuda para el trabajo interno y algunos de ellos se ampliaron bastante lejos, pero al recordar los principios básicos de SOLID, la mayoría de ese código salió muy mantenible y flexible. Al igual que en OOP, su interfaz es una clase y mueve las clases a medida que refactoriza y / o agrega funcionalidad, en FP hace exactamente lo mismo con las funciones.

La semana pasada comencé a codificar en Java y desde entonces, casi a diario, me recuerdan que cuando estoy en OOP, tengo que implementar interfaces declarando clases con métodos que anulan funciones, en algunos casos podría lograr lo mismo en Python usando un La expresión lambda simple, por ejemplo, 20-30 líneas de código que escribí para escanear un directorio, habría sido de 1-2 líneas en Python y sin clases.

FP en sí mismos son idiomas de nivel superior. En Python (lo siento, mi única experiencia de FP) podría reunir la comprensión de la lista dentro de otra comprensión de la lista con lambdas y otras cosas agregadas y todo sería solo 3-4 líneas de código. En C ++, podría lograr absolutamente lo mismo, pero debido a que C ++ es de nivel inferior, tendría que escribir mucho más código que 3-4 líneas y a medida que aumenta el número de líneas, mi entrenamiento SRP comenzaría y comenzaría pensando en cómo dividir el código en partes más pequeñas (es decir, más funciones). Pero en aras de la mantenibilidad y la ocultación de los detalles de implementación, pondría todas esas funciones en la misma clase y las haría privadas. Y ahí lo tienen ... Acabo de crear una clase, mientras que en Python habría escrito "return (.... lambda x: .. ....)"

DXM
fuente
Sí, no responde directamente a la pregunta, pero sigue siendo una gran respuesta. cuando escribo scripts o paquetes más pequeños en python, tampoco siempre uso clases. muchas veces simplemente tenerlo en formato de paquete encaja perfectamente. especialmente si no necesito estado. También estoy de acuerdo en que las comprensiones de listas también son muy útiles. Desde que leí sobre FP, me di cuenta de lo mucho más poderosos que pueden ser. lo que me llevó a querer aprender más sobre FP, en comparación con OOP.
Skift
Gran respuesta. Habla con todos los que están parados al lado de la piscina funcional pero no están seguros de si sumergir el dedo del pie en el agua
Robben_Ford_Fan_boy
Y Ruby ... Una de sus filosofías de diseño se centra en los métodos que toman un bloque de código como argumento, generalmente uno opcional. Y dada la sintaxis limpia, pensar y codificar de esta manera es fácil. Es difícil pensar y componer así en C #. C # write es funcionalmente detallado y desorientador, se siente como un zapato en el lenguaje. Me gusta que Ruby ayudó a pensar funcionalmente más fácil, para ver el potencial en mi cuadro de pensamiento C #. En el análisis final, veo funcional y OO como complementario; Yo diría que Ruby ciertamente cree que sí.
radarbob
8

En Haskell, lo más cercano que tienes es "clase". Esta clase, aunque no es igual a la clase en Java y C ++ , funcionará para lo que desea en este caso.

En su caso, así es como se verá su código.

clase Animal a donde 
say :: String -> sonido 

Entonces puede tener tipos de datos individuales adaptando estos métodos.

instancia Animal Dog donde
decir s = "ladrar" ++ s 

EDITAR: - Antes de que pueda especializarse, digamos para Dog, debe decirle al sistema que Dog es animal.

data Dog = \ - algo aquí - \ (derivando Animal)

EDITAR: - Para Wilq.
Ahora, si quieres usar say en una función say foo, tendrás que decirle a haskell que foo solo puede trabajar con Animal.

foo :: (Animal a) => a -> Cadena -> Cadena
foo a str = decir un str 

ahora, si llamas a foo con perro, ladrará, si llamas con gato, maullaría.

main = do 
let d = dog (\ - parámetros cstr - \)
    c = gato  
en el show $ foo d "Hola mundo"

Ahora no puede tener ninguna otra definición de función, por ejemplo. Si se llama a say con algo que no sea animal, provocará un error de compilación.

Manoj R
fuente
Tendré que entender un poco más sobre Haskell para entenderlo completamente, pero creo que entiendo lo mejor. Todavía tengo curiosidad de cómo esto se alinearía con una base de código más compleja.
skift
nitpick Animal debe estar en mayúscula
Daniel Gratzer
1
¿Cómo sabe la función say que lo llamas en un perro si solo toma una cadena? ¿Y no está "derivando" solo para algunas clases integradas?
WilQu
6

Los lenguajes funcionales usan 2 construcciones para lograr el polimorfismo:

  • Funciones de primer orden
  • Genéricos

Crear código polimórfico con esos es completamente diferente de cómo OOP usa la herencia y los métodos virtuales. Si bien ambos pueden estar disponibles en su lenguaje OOP favorito (como C #), la mayoría de los lenguajes funcionales (como Haskell) aumentan a once. Es raro que la función sea no genérica y la mayoría de las funciones tienen funciones como parámetros.

Es difícil de explicar de esta manera y requerirá mucho tiempo aprender esta nueva forma. Pero para hacer esto, debe olvidar completamente la POO, porque no es así como funciona en el mundo funcional.

Eufórico
fuente
2
OOP tiene que ver con el polimorfismo. Si cree que OOP se trata de tener funciones vinculadas a sus datos, entonces no sabe nada sobre OOP.
Eufórico
44
El polimorfismo es solo un aspecto de la POO, y creo que no es el único por el que el OP realmente está preguntando.
Doc Brown
2
El polimorfismo es un aspecto clave de la POO. Todo lo demás está ahí para apoyarlo. OOP sin herencia / métodos virtuales es casi exactamente lo mismo que la programación de procedimientos.
Eufórico
1
@ErikReppen Si no es necesario "aplicar una interfaz" con frecuencia, entonces no está haciendo POO. Además, Haskell también tiene módulos.
Eufórico
1
No siempre necesitas una interfaz. Pero son muy útiles cuando los necesita. Y la OMI, otra parte importante de OOP. En cuanto a los módulos que se encuentran en Haskell, creo que esto probablemente esté más cerca de OOP para lenguajes funcionales, en lo que respecta a la organización del código. Al menos por lo que he leído hasta ahora. Sé que todavía son muy diferentes.
skift
0

realmente depende de lo que quieras lograr.

Si solo necesita una forma de organizar el comportamiento en función de criterios selectivos, puede utilizar, por ejemplo, un diccionario (tabla hash) con objetos de función. en python podría ser algo como:

def bark(what):
    print "barks: {0}".format(what) 

def meow(what):
    print "meows: {0}".format(what)

def climb(how):
    print "climbs: {0}".format(how)

if __name__ == "__main__":
    animals = {'dog': {'say': bark},
               'cat': {'say': meow,
                       'climb': climb}}
    animals['dog']['say']("ruff")
    animals['cat']['say']("purr")
    animals['cat']['climb']("well")

Sin embargo, tenga en cuenta que (a) no hay 'instancias' de perros o gatos y (b) tendrá que hacer un seguimiento del 'tipo' de sus objetos usted mismo.

como por ejemplo: pets = [['martin','dog','grrrh'], ['martha', 'cat', 'zzzz']]. entonces podrías hacer una lista de comprensión como[animals[pet[1]]['say'](pet[2]) for pet in pets]

kr1
fuente
0

Los idiomas OO se pueden usar en lugar de idiomas de bajo nivel a veces para interactuar directamente con una máquina. C ++ Seguro, pero incluso para C # hay adaptadores y demás. Aunque escribir código para controlar las partes mecánicas y tener un control minucioso sobre la memoria se mantiene lo más cerca posible del nivel más bajo posible. Pero si esta pregunta está relacionada con el software actual orientado a objetos como Line Of Business, aplicaciones web, IOT, servicios web y la mayoría de las aplicaciones de uso masivo, entonces ...

Respuesta, si corresponde

Los lectores pueden intentar trabajar con una arquitectura orientada a servicios (SOA). Es decir, DDD, N-Layered, N-Tiered, Hexagonal, lo que sea. No he visto que una aplicación para grandes empresas utilice eficientemente OO "tradicional" (Active-Record o Rich-Models) como se describió en los años 70 y 80 en la última década. (Ver nota 1)

La falla no es con el OP, pero hay un par de problemas con la pregunta.

  1. El ejemplo que proporciona es simplemente para demostrar Polimorfismo, no es un código de producción. A veces, ejemplos exactamente así se toman literalmente.

  2. En FP y SOA, los datos se separan de la lógica de negocios. Es decir, los datos y la lógica no van juntos. La lógica entra en Servicios, y los Datos (Modelos de dominio) no tienen comportamiento Polimórfico (Ver Nota 2).

  3. Los servicios y funciones pueden ser polimórficos. En FP, con frecuencia pasa funciones como parámetros a otras funciones en lugar de valores. Puede hacer lo mismo en OO Languages ​​con tipos como Callable o Func, pero no se ejecuta desenfrenada (Ver Nota 3). En FP y SOA, sus modelos no son polimórficos, solo sus servicios / funciones. (Ver nota 4)

  4. Hay un mal caso de hardcoding en ese ejemplo. No solo estoy hablando de la cadena de color rojo "perro ladra". También estoy hablando de CatModel y DogModel. ¿Qué sucede cuando quieres agregar una oveja? ¿Tienes que ingresar tu código y crear un nuevo código? ¿Por qué? En el código de producción, preferiría ver solo un AnimalModel con sus propiedades. En el peor de los casos, un modelo de anfibio y un modelo de ave si sus propiedades y manejo son tan diferentes.

Esto es lo que esperaría ver en un lenguaje "OO" actual:

public class Animal
{
    public int AnimalID { get; set; }
    public int LegCount { get; set; }
    public string Name { get; set; }
    public string WhatISay { get; set; }
}

public class AnimalService : IManageAnimals
{
    private IPersistAnimals _animalRepo;
    public AnimalService(IPersistAnimals animalRepo) { _animalRepo = animalRepo; }

    public List<Animal> GetAnimals() => _animalRepo.GetAnimals();

    public string WhatDoISay(Animal animal)
    {
        if (!string.IsNullOrWhiteSpace(animal.WhatISay))
            return animal.WhatISay;

        return _animalRepo.GetAnimalNoise(animal.AnimalID);
    }
}

Flujo básico

¿Cómo pasar de las clases en OO a la programación funcional? Como otros han dicho; Puedes, pero en realidad no. El objetivo de lo anterior es demostrar que ni siquiera debería usar Clases (en el sentido tradicional del mundo) al hacer Java y C #. Una vez que llegue a escribir código en una Arquitectura Orientada a Servicios (DDD, Capas, Niveles, Hexagonal, lo que sea), estará un paso más cerca de Funcional porque separa sus Datos (Modelos de Dominio) de sus Funciones Lógicas (Servicios).

OO Language un paso más cerca de FP

Incluso podría llevarlo un poco más lejos y dividir sus Servicios SOA en dos tipos.

Clase opcional Tipo 1 : Servicios comunes de implementación de interfaz para puntos de entrada. Estos serían puntos de entrada "impuros" que pueden llamar a otras funciones "puras" o "impuras". Estos podrían ser sus puntos de entrada de una API RESTful.

Clase opcional tipo 2 : Servicios de lógica empresarial pura. Estas son clases estáticas que tienen funcionalidad "pura". En FP, "Puro" significa que no hay efectos secundarios. No establece explícitamente el estado o la persistencia en ninguna parte. (Ver nota 5)

Entonces, cuando piensa en las Clases en Lenguajes Orientados a Objetos, que se usan en una Arquitectura Orientada a Servicios, no solo beneficia su Código OO, sino que comienza a hacer que la Programación Funcional parezca muy fácil de entender.

Notas

Nota 1 : El diseño orientado a objetos "Rich" o "Active-Record" original todavía existe. Hay MUCHO código heredado como ese cuando la gente "lo hacía bien" hace una década o más. La última vez que vi ese tipo de código (hecho correctamente) era de un video juego Codebase en C ++ donde controlaban la memoria con precisión y tenían un espacio muy limitado. No quiere decir que FP y las arquitecturas orientadas a servicios sean bestias y no deberían considerar el hardware. Pero colocan la capacidad de cambiar constantemente, mantenerse, tener tamaños de datos variables y otros aspectos como prioridad. En los videojuegos y la IA de la máquina, controlas las señales y los datos con mucha precisión.

Nota 2 : Los modelos de dominio no tienen comportamiento polimórfico, ni tienen dependencias externas. Están "aislados". Eso no significa que tengan que ser 100% anémicos. Pueden tener mucha lógica relacionada con su construcción y alteración de propiedades mutables, si corresponde. Ver DDD "Objetos de valor" y entidades de Eric Evans y Mark Seemann.

Nota 3 : Linq y Lambda son muy comunes. Pero cuando un usuario crea una nueva función, rara vez usa Func o Callable como parámetros, mientras que en FP sería extraño ver una aplicación sin funciones que sigan ese patrón.

Nota 4 : No confundir el polimorfismo con la herencia. Un CatModel podría heredar AnimalBase para determinar qué propiedades tiene típicamente un animal. Pero como muestro, Modelos como este son un código de olor . Si ve este patrón, podría considerar desglosarlo y convertirlo en datos.

Nota 5 : Las funciones puras pueden (y lo hacen) aceptar funciones como parámetros. La función entrante puede ser impura, pero puede ser pura. Para fines de prueba, siempre sería puro. Pero en la producción, aunque se trata como puro, puede contener efectos secundarios. Eso no cambia el hecho de que la función pura es pura. Aunque la función del parámetro puede ser impura. No es confuso! :RE

Suamere
fuente
-2

Podrías hacer algo como esto ... php

    function say($whostosay)
    {
        if($whostosay == 'cat')
        {
             return 'purr';
        }elseif($whostosay == 'dog'){
             return 'bark';
        }else{
             //do something with errors....
        }
     }

     function speak($whostosay)
     {
          return $whostosay .'\'s '.say($whostosay);
     }
     echo speak('cat');
     >>>cat's purr
     echo speak('dog');
     >>>dogs's bark
Michael Dennis
fuente
1
No he dado ningún voto negativo. Pero supongo que es porque este enfoque no es funcional ni orientado a objetos.
Manoj R
1
Pero el concepto transmitido está cerca de la coincidencia de patrones utilizada en la programación funcional, es decir, se $whostosayconvierte en el tipo de objeto que determina lo que se ejecuta. Lo anterior se puede modificar para aceptar adicionalmente otro parámetro $whattosaypara que un tipo que lo admite (por ejemplo 'human') pueda utilizarlo.
syockit