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 Bird
clase 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í).
fuente
Respuestas:
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í:
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.
No, esto está
Monad
en Haskell: un tipo parametrizadom a
con una implementación dereturn :: a -> m a
y que(>>=) :: m a -> (a -> m b) -> m b
cumple con las siguientes leyes: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.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:
Creo que este es un patrón conocido como "interfaz fluida" o "encadenamiento de métodos" (pero no estoy seguro).
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: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:
fuente
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".unit
convierte (principalmente) en un constructor del tipo contenido, y sebind
convierte (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 unbind
método puede hacer un enlace tardío, pero tomaré ese abuso a continuación. ;)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.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>
yFlier<Bird>
tener implementaciones diferentes. Tienes que hacer algo comostatic 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íanFlier<Bird>
seguir siendo aBird
.fuente
Flier<Bird>
es un contenedor parametrizado, nadie lo consideraría como unBird
(!?)List<String>
Es una Lista, no una Cadena.Flier
no es solo un contenedor. Si lo considera solo un contenedor, ¿por qué pensaría que podría reemplazar el uso de la herencia?Animal / Bird / Penguin
suele 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()
Resource
agrega semántica encimaString
(u otro tipo), por lo que obviamente no es unaString
.