scala - monada programacion
¿Por qué Scala no tiene una función de retorno/unidad definida para cada mónada(en contraste con Haskell)? (4)
¿Cuál es la razón detrás de la decisión de diseño en Scala de que las mónadas no tienen una función de retorno / unidad en contraste con Haskell donde cada mónada tiene una función de retorno que pone un valor en un contexto monádico estándar para la mónada dada?
Por ejemplo, ¿por qué List, Option, Set, etc ... no tienen funciones de retorno / unidad definidas en la biblioteca estándar como se muestra en las diapositivas a continuación?
Lo pregunto porque en el curso de Coursera reactivo, Martin Odersky mencionó explícitamente este hecho, como se puede ver a continuación en las diapositivas, pero no explicó por qué Scala no los tiene, aunque la unidad / retorno es una propiedad esencial de una mónada.
Advertencia: todavía estoy aprendiendo Haskell y estoy inventando esta respuesta a medida que avanzo.
En primer lugar, lo que ya sabes, que Haskell do
notas de notación para unir :
Tomando prestado este ejemplo de Wikipedia:
add mx my = do
x <- mx
y <- my
return (x + y)
add mx my =
mx >>= (/x ->
my >>= (/y ->
return (x + y)))
El análogo de Scala a do
es la expresión de rendimiento . De manera similar, se desgasta cada paso a flatMap
(su equivalente a bind).
Sin embargo, hay una diferencia: la última <-
en una desugars de rendimiento para map
, no para flatMap
.
def add(mx: Option[Int], my: Option[Int]) =
for {
x <- mx
y <- my
} yield x + y
def add(mx: Option[Int], my: Option[Int]) =
mx.flatMap(x =>
my.map(y =>
x + y))
Entonces, debido a que no tiene el "aplanamiento" en el último paso, el valor de la expresión ya tiene el tipo de mónada, por lo que no es necesario "reenvolverlo" con algo comparable a return
.
Como dijo Ørjan Johansen, Scala no admite el envío de métodos en el tipo de devolución. El sistema de objetos Scala está construido sobre JVM uno, y la instrucción invokevirtual
JVM, que es la herramienta principal para el polimorfismo dinámico, distribuye la llamada según el tipo de this
objeto.
Como nota al margen, el envío es un proceso de selección de un método concreto para llamar. En Scala / Java, todos los métodos son virtuales, es decir, el método real al que se llama depende del tipo real del objeto.
class A { def hello() = println("hello method in A") }
class B extends A { override def hello() = println("hello method in B") }
val x: A = new A
x.hello() // prints "hello method in A"
val y: A = new B
y.hello() // prints "hello method in B"
Aquí, incluso si y
variable es de tipo A
, se llama al método hello
de B
, porque JVM "ve" que el tipo real del objeto en y
es B
e invoca el método apropiado.
Sin embargo, JVM solo toma el tipo de la variable en la que se llama al método. Es imposible, por ejemplo, llamar a diferentes métodos basados en el tipo de argumentos de tiempo de ejecución sin controles explícitos. Por ejemplo:
class A {
def hello(x: Number) = println(s"Number: $x")
def hello(y: Int) = println(s"Integer: $y")
}
val a = new A
val n: Number = 10: Int
a.hello(n) // prints "Number: 10"
Aquí tenemos dos métodos con el mismo nombre, pero con un tipo de parámetro diferente. E incluso si n
el tipo real es Int
, se llama la versión hello(Number)
; se resuelve de forma estática en función de n
tipo de variable estática (esta función, resolución estática basada en tipos de argumentos, se denomina sobrecarga). Por lo tanto, no hay un envío dinámico en los argumentos del método. Algunos lenguajes también admiten el envío de argumentos de métodos, por ejemplo, el CLOS de Common Lisp o los métodos múltiples de Clojure funcionan así.
Haskell tiene un sistema de tipo avanzado (es comparable al de Scala y, de hecho, ambos se originan en el Sistema F , pero el sistema de tipo Scala admite subtipos, lo que hace que la inferencia de tipos sea mucho más difícil), lo que permite la inferencia de tipo global, al menos, sin ciertas extensiones habilitadas. Haskell también tiene un concepto de clases de tipo, que es su herramienta para el polimorfismo dinámico. Las clases de tipos se pueden considerar como interfaces sin herencia pero con despacho en los tipos de parámetros y valores de retorno . Por ejemplo, esta es una clase de tipo válida:
class Read a where
read :: String -> a
instance Read Integer where
read s = -- parse a string into an integer
instance Read Double where
read s = -- parse a string into a double
Luego, dependiendo del contexto donde se llame al método, la función de read
para Integer
o Double
se puede llamar:
x :: Integer
x = read "12345" // read for Integer is called
y :: Double
y = read "12345.0" // read for Double is called
Esta es una técnica muy poderosa que no tiene correspondencia en el sistema de objetos JVM, por lo que el sistema de objetos Scala no lo admite también. Además, la falta de inferencia de tipo de escala completa haría que esta característica fuera algo incómoda de usar. Por lo tanto, la biblioteca estándar de Scala no tiene un método de return
/ unit
ninguna parte: es imposible expresarlo utilizando un sistema de objetos normal, simplemente no hay lugar donde se pueda definir dicho método. En consecuencia, el concepto de mónada en Scala es implícito y convencional: todo con el método flatMap
apropiado puede considerarse una mónada y todo con los métodos correctos se puede usar for
construcción. Esto es muy parecido a escribir pato.
Sin embargo, el sistema de tipo Scala junto con su mecanismo implícito es lo suficientemente poderoso como para expresar clases de tipo con todas las funciones y, por extensión, mónadas genéricas de manera formal, aunque debido a dificultades en la inferencia de tipo completo puede requerir agregar más anotaciones de tipo que en Haskell .
Esta es la definición de la clase de tipo de mónada en Scala:
trait Monad[M[_]] {
def unit[A](a: A): M[A]
def bind[A, B](ma: M[A])(f: A => M[B]): M[B]
}
Y esta es su implementación para Option
:
implicit object OptionMonad extends Monad[Option] {
def unit[A](a: A) = Some(a)
def bind[A, B](ma: Option[A])(f: A => Option[B]): Option[B] =
ma.flatMap(f)
}
Entonces esto puede ser usado de manera genérica como esta:
// note M[_]: Monad context bound
// this is a port of Haskell''s filterM found here:
// http://hackage.haskell.org/package/base-4.7.0.1/docs/src/Control-Monad.html#filterM
def filterM[M[_]: Monad, A](as: Seq[A])(f: A => M[Boolean]): M[Seq[A]] = {
val m = implicitly[Monad[M]]
as match {
case x +: xs =>
m.bind(f(x)) { flg =>
m.bind(filterM(xs)(f)) { ys =>
m.unit(if (flg) x +: ys else ys)
}
}
case _ => m.unit(Seq.empty[A])
}
}
// using it
def run(s: Seq[Int]) = {
import whatever.OptionMonad // bring type class instance into scope
// leave all even numbers in the list, but fail if the list contains 13
filterM[Option, Int](s) { a =>
if (a == 13) None
else if (a % 2 == 0) Some(true)
else Some(false)
}
}
run(1 to 16) // returns None
run(16 to 32) // returns Some(List(16, 18, 20, 22, 24, 26, 28, 30, 32))
Aquí, filterM
se escribe genéricamente, para cualquier instancia de clase de tipo Monad
. Debido a que el objeto implícito OptionMonad
está presente en el sitio de llamada filterM
, se pasará a filterM
implícitamente y podrá hacer uso de sus métodos.
Puede ver desde arriba que las clases de tipos permiten emular el envío en el tipo de retorno incluso en Scala. De hecho, esto es exactamente lo que Haskell hace bajo la cobertura: tanto Scala como Haskell están pasando un diccionario de métodos que implementan algún tipo de clase, aunque en Scala es algo más explícito porque estos "diccionarios" son objetos de primera clase allí y pueden ser importado a pedido o incluso pasado explícitamente, por lo que no es realmente un envío adecuado, ya que no está tan integrado.
Si necesita esta cantidad de genérico, puede usar la biblioteca Scalaz que contiene muchas clases de tipos (incluida la mónada) y sus instancias para algunos tipos comunes, incluida la Option
.
En realidad hay una función de retorno en Scala. Es difícil de encontrar.
Scala difiere ligeramente de Haskell en muchos aspectos. La mayoría de esas diferencias son consecuencias directas de las limitaciones de JVM. JVM no puede enviar métodos basándose en su tipo de devolución. Así que Scala introdujo el polimorfismo de clase de tipo basado en evidencia implícita para solucionar este inconveniente.
Incluso se utiliza en las colecciones estándar de Scala. Es posible que CanBuildFrom
numerosos usos de las CanBuildFrom
de CanBuildFrom
y CanBuild
utilizadas en la colección de la API de CanBuildFrom
. Ver scala.collection.immutable.List por ejemplo.
Cada vez que desee crear una colección personalizada, debe escribir la realización para estas implicaciones. Sin embargo, no hay tantas guías para escribir una. Te recomiendo esta guía . Muestra por qué CanBuildFrom
es tan importante para las colecciones y cómo se utiliza. De hecho, esa es solo otra forma de la función de return
y cualquier persona familiarizada con las mónadas de Haskell entendería su importancia claramente.
Por lo tanto, puede usar la colección personalizada como mónadas de ejemplo y escribir otras mónadas basándose en el tutorial proporcionado.
No creo que realmente estés diciendo que las mónadas de Scala no tienen una función de unidad, es más bien que el nombre de la función de unidad puede variar. Eso es lo que parece mostrarse en los ejemplos de la segunda diapositiva.
En cuanto a por qué es así, creo que es solo porque Scala se ejecuta en la JVM, y esas funciones deben implementarse como métodos de JVM, que se identifican de forma única por:
- la clase a la que pertenecen;
- su nombre;
- sus tipos de parámetros. Pero no se identifican por su tipo de retorno. Como el tipo de parámetro generalmente no diferenciará las distintas funciones de la unidad (generalmente es solo un tipo genérico), necesita diferentes nombres para ellas.
En la práctica, a menudo se implementarán como el método apply(x)
en el objeto compañero de la clase mónada. Por ejemplo, para la List
clases, la función de unidad es el método apply(x)
en la List
objetos. Por convención, List.apply(x)
se puede llamar como List(x)
, que es más común / idiomático.
Así que supongo que Scala tiene al menos una convención de nomenclatura para la función de la unidad, aunque no tiene un nombre único para ella:
// Some monad :
class M[T] {
def flatMap[U](f: T => M[U]): M[U] = ???
}
// Companion object :
object M {
def apply(x: T): M[T] = ??? // Unit function
}
// Usage of the unit function :
val x = ???
val m = M(x)