¿Son las mónadas una alternativa viable (quizás preferible) a las jerarquías de herencia?

20

Voy a utilizar una descripción independiente del lenguaje de mónadas como esta, describiendo primero los monoides:

Un monoide es (aproximadamente) un conjunto de funciones que toman algún tipo de parámetro y devuelven el mismo tipo.

Una mónada es (aproximadamente) un conjunto de funciones que toman un tipo de contenedor como parámetro y devuelve el mismo tipo de contenedor.

Tenga en cuenta que esas son descripciones, no definiciones. ¡Siéntase libre de atacar esa descripción!

Entonces, en un lenguaje OO, una mónada permite composiciones de operaciones como:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

Tenga en cuenta que la mónada define y controla la semántica de esas operaciones, en lugar de la clase contenida.

Tradicionalmente, en un lenguaje OO, usaríamos una jerarquía de clases y herencia para proporcionar esa semántica. Así tendríamos una Birdclase con métodos takeOff(), flyAround()y land(), y el Pato heredaríamos aquellos.

Pero luego nos metemos en problemas con las aves no voladoras, porque penguin.takeOff()falla. Tenemos que recurrir al lanzamiento y manejo de excepciones.

Además, una vez que decimos que Penguin es a Bird, nos encontramos con problemas de herencia múltiple, por ejemplo, si también tenemos una jerarquía de Swimmer.

Esencialmente estamos tratando de poner las clases en categorías (con disculpas a los muchachos de la teoría de la categoría) y definir la semántica por categoría en lugar de en clases individuales. Pero las mónadas parecen un mecanismo mucho más claro para hacerlo que las jerarquías.

Entonces, en este caso, tendríamos una Flier<T>mónada como el ejemplo anterior:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

... y nunca instanciaríamos a Flier<Penguin>. Incluso podríamos usar la escritura estática para evitar que eso suceda, tal vez con una interfaz de marcador. O la verificación de la capacidad de tiempo de ejecución para rescatar. Pero en realidad, un programador nunca debe poner un pingüino en el volante, en el mismo sentido, nunca debe dividir por cero.

Además, es más generalmente aplicable. Un viajero no tiene que ser un pájaro. Por ejemplo Flier<Pterodactyl>, o Flier<Squirrel>, sin cambiar la semántica de esos tipos individuales.

Una vez que clasificamos la semántica por funciones componibles en un contenedor, en lugar de con jerarquías de tipos, resuelve los viejos problemas con las clases que "tipo de hacer, tipo de no" encajan en una jerarquía particular. También permite fácil y claramente múltiples semánticas para una clase, así Flier<Duck>como también Swimmer<Duck>. Parece que hemos estado luchando con un desajuste de impedancia al clasificar el comportamiento con las jerarquías de clase. Las mónadas lo manejan con elegancia.

Entonces, mi pregunta es, de la misma manera que hemos llegado a favorecer la composición sobre la herencia, ¿también tiene sentido favorecer a las mónadas sobre la herencia?

(Por cierto, no estaba seguro de si esto debería estar aquí o en Comp Sci, pero esto parece más un problema de modelado práctico. Pero tal vez sea mejor allí).

Robar
fuente
1
No estoy seguro de entender cómo funciona: una ardilla y un pato no vuelan de la misma manera, por lo que la "acción de volar" debe implementarse en esas clases ... Y el viajero necesita un método para hacer que la ardilla y el pato volar ... Tal vez en una interfaz de viajero común ... Vaya, espere un minuto ... ¿Me perdí algo?
assylias
Las interfaces son diferentes a la herencia de clase, porque las interfaces definen capacidades mientras que la herencia funcional define el comportamiento real. Incluso en "composición sobre herencia", definir interfaces sigue siendo un mecanismo importante (por ejemplo, polimorfismo). Las interfaces no se encuentran con los mismos problemas de herencia múltiple. Además, cada volante podría proporcionar (a través de una interfaz y polimorfismo) propiedades de capacidad como "getFlightSpeed ​​()" o "getManuverability ()" para que el contenedor las use.
Rob
3
¿Intenta preguntarse si el uso de polimorfismo paramétrico es siempre una alternativa viable al polimorfismo de subtipo?
ChaosPandion
sí, con la arruga de agregar funciones componibles que preservan la semántica. Los tipos de contenedores parametrizados han existido durante mucho tiempo, pero por sí mismos no me parecen una respuesta completa. Por eso me pregunto si el patrón de mónada tiene un papel más fundamental que desempeñar.
Rob
66
No entiendo tu descripción de monoides y mónadas. La propiedad clave de los monoides es que implica una operación binaria asociativa (piense en la adición de punto flotante, la multiplicación de enteros o la concatenación de cadenas). Una mónada es una abstracción que admite la secuenciación de varios cálculos (posiblemente dependientes) en algún orden.
Rufflewind

Respuestas:

15

La respuesta corta es no , las mónadas no son una alternativa a las jerarquías de herencia (también conocido como polimorfismo de subtipo). Parece que está describiendo el polimorfismo paramétrico , que las mónadas utilizan pero no son lo único que puede hacer.

Por lo que yo entiendo, las mónadas no tienen esencialmente nada que ver con la herencia. Yo diría que las dos cosas son más o menos ortogonales: están destinadas a abordar diferentes problemas, y así:

  1. Se pueden usar sinérgicamente en al menos dos sentidos:
    • echa un vistazo a Typeclassopedia , que cubre muchas de las clases de tipos de Haskell. Notarás que hay relaciones de herencia entre ellos. Por ejemplo, Monad desciende de Applicative, que desciende de Functor.
    • Los tipos de datos que son instancias de Mónadas pueden participar en las jerarquías de clases. Recuerde, Monad es más como una interfaz: implementarlo para un tipo dado le dice algunas cosas sobre el tipo de datos, pero no todo.
  2. Intentar usar uno para hacer el otro será difícil y feo.

Finalmente, aunque esto es tangencial a su pregunta, puede interesarle saber que las mónadas tienen formas increíblemente poderosas de componer; lea sobre transformadores de mónada para obtener más información. Sin embargo, esta sigue siendo un área activa de investigación porque nosotros (y por nosotros, me refiero a personas 100000 veces más inteligentes que yo) no hemos descubierto grandes formas de componer mónadas, y parece que algunas mónadas no componen arbitrariamente.


Ahora, para aclarar su pregunta (lo siento, tengo la intención de que esto sea útil, y no que lo haga sentir mal): siento que hay muchas premisas cuestionables sobre las que intentaré arrojar algo de luz.

  1. Una mónada es un conjunto de funciones que toman un tipo de contenedor como parámetro y devuelve el mismo tipo de contenedor.

    No, esto está Monaden Haskell: un tipo parametrizado m acon una implementación de return :: a -> m ay que (>>=) :: m a -> (a -> m b) -> m bcumple con las siguientes leyes:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    Hay algunas instancias de Monad que no son contenedores ( (->) b), y hay algunos contenedores que no son (y no se pueden hacer) instancias de Monad ( Set, debido a la restricción de clase de tipo). Entonces la intuición del "contenedor" es pobre. Vea esto para más ejemplos.

  2. Entonces, en un lenguaje OO, una mónada permite composiciones de operaciones como:

      Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
    

    No, en absoluto. Ese ejemplo no requiere una mónada. Todo lo que requiere es funciones con tipos de entrada y salida coincidentes. Aquí hay otra forma de escribirlo que enfatiza que es solo una aplicación de función:

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    Creo que este es un patrón conocido como "interfaz fluida" o "encadenamiento de métodos" (pero no estoy seguro).

  3. Tenga en cuenta que la mónada define y controla la semántica de esas operaciones, en lugar de la clase contenida.

    Los tipos de datos que también son mónadas pueden (¡y casi siempre lo hacen!) Tener operaciones que no están relacionadas con las mónadas. Aquí hay un ejemplo de Haskell compuesto por tres funciones en las []que no tiene nada que ver con las mónadas: []"define y controla la semántica de la operación" y la "clase contenida" no, pero eso no es suficiente para hacer una mónada:

    \predicate -> length . filter predicate . reverse
    
  4. Ha notado correctamente que hay problemas con el uso de jerarquías de clases para modelar cosas. Sin embargo, sus ejemplos no presentan ninguna evidencia de que las mónadas puedan:

    • Haz un buen trabajo en esas cosas en las que la herencia es buena
    • Haz un buen trabajo en esas cosas en las que la herencia es mala
Comunidad
fuente
3
¡Gracias! Hay mucho que procesar. No me siento mal, aprecio mucho la idea. Me sentiría peor llevando las malas ideas. :) (¡Va al punto completo de stackexchange!)
Rob
1
@RobY De nada! Por cierto, si no has oído hablar de él antes, te recomiendo LYAH, ya que es una gran fuente para aprender mónadas (¡y Haskell!) Porque tiene toneladas de ejemplos (y creo que hacer toneladas de ejemplos es la mejor manera de abordar mónadas).
Hay mucho aquí; No quiero saturar los comentarios, pero algunos comentarios: # 2 land(flyAround(takeOff(new Flier<Duck>(duck))))no funciona (al menos en OO) porque esa construcción requiere romper la encapsulación para obtener los detalles de Flier. Al encadenar operaciones en la clase, los detalles de Flier permanecen ocultos y puede preservar su semántica. Eso es similar a la razón por la cual en Haskell se une una mónada (a, M b)y no (M a, M b)para que la mónada no tenga que exponer su estado a la función de "acción".
Rob
# 1, desafortunadamente estoy tratando de difuminar la definición estricta de Monad en Haskell, porque asignar cualquier cosa a Haskell tiene un gran problema: la composición de funciones, incluida la composición en constructores , que no se puede hacer fácilmente en un lenguaje peatonal como Java. Por lo tanto, se unitconvierte (principalmente) en un constructor del tipo contenido, y se bindconvierte (principalmente) en una operación de tiempo de compilación implícita (es decir, enlace temprano) que vincula las funciones de "acción" a la clase. Si tiene funciones de primera clase, o una función Function <A, Monad <B>>, entonces un bindmétodo puede hacer un enlace tardío, pero tomaré ese abuso a continuación. ;)
Rob
# 3 de acuerdo, y esa es la belleza. Si Flier<Thing>controla la semántica del vuelo, puede exponer una gran cantidad de datos y operaciones que mantienen la semántica del vuelo, mientras que la semántica específica de la "mónada" se trata realmente de encadenarlo y encapsularlo. Esas preocupaciones pueden no ser (y con las que he estado usando, no son) preocupaciones de la clase dentro de la mónada: por ejemplo, Resource<String>tiene una propiedad httpStatus, pero String no.
Rob
1

Entonces, mi pregunta es, de la misma manera que hemos llegado a favorecer la composición sobre la herencia, ¿también tiene sentido favorecer a las mónadas sobre la herencia?

En idiomas que no son OO, sí. En los idiomas OO más tradicionales, diría que no.

El problema es que la mayoría de los idiomas no tienen especialización de tipo, lo que significa que no puede hacer Flier<Squirrel>y Flier<Bird>tener implementaciones diferentes. Tienes que hacer algo como static Flier Flier::Create(Squirrel)(y luego sobrecargar para cada tipo). Lo que a su vez significa que debe modificar este tipo cada vez que agrega un nuevo animal, y probablemente duplica un poco de código para que funcione.

Ah, y en no pocos idiomas (C # por ejemplo) public class Flier<T> : T {}es ilegal. Ni siquiera se construirá. La mayoría, si no todos los programadores de OO esperarían Flier<Bird>seguir siendo a Bird.

Telastyn
fuente
gracias por el comentario. Tengo algunas ideas más, pero solo trivialmente, a pesar de que Flier<Bird>es un contenedor parametrizado, nadie lo consideraría como un Bird(!?) List<String>Es una Lista, no una Cadena.
Rob
@RobY: Flierno es solo un contenedor. Si lo considera solo un contenedor, ¿por qué pensaría que podría reemplazar el uso de la herencia?
Telastyn
Te perdí allí ... mi punto es que la mónada es un contenedor mejorado. Animal / Bird / Penguinsuele ser un mal ejemplo, porque trae todo tipo de semántica. Un ejemplo práctico es una mónada REST-ish que estamos usando: Resource<String>.from(uri).get() Resourceagrega semántica encima String(u otro tipo), por lo que obviamente no es una String.
Rob
@RobY, pero tampoco está relacionado con la herencia.
Telastyn
Excepto que es un tipo diferente de contención. Puedo poner String en Resource, o podría abstraer una clase ResourceString y usar la herencia. Mi pensamiento es que poner una clase en un contenedor de encadenamiento es una mejor manera de abstraer el comportamiento que ponerlo en una jerarquía de clases con herencia. Entonces, "de ninguna manera relacionada" en el sentido de "reemplazar / obviar" - sí.
Rob