scala functional-programming applicative

¿Cuándo y por qué debería uno usar Funcionales Aplicativos en Scala?



functional-programming applicative (2)

Sé que Monad se puede expresar en Scala de la siguiente manera:

trait Monad[F[_]] { def flatMap[A, B](f: A => F[B]): F[A] => F[B] }

Veo por qué es útil. Por ejemplo, dadas dos funciones:

getUserById(userId: Int): Option[User] = ... getPhone(user: User): Option[Phone] = ...

Puedo escribir fácilmente la función getPhoneByUserId(userId: Int) ya que Option es una mónada:

def getPhoneByUserId(userId: Int): Option[Phone] = getUserById(userId).flatMap(user => getPhone(user))

...

Ahora veo el Applicative Functor en Scala:

trait Applicative[F[_]] { def apply[A, B](f: F[A => B]): F[A] => F[B] }

Me pregunto cuándo debería usarlo en lugar de mónada. Supongo que tanto la Opción como la Lista son Applicatives . ¿Podría dar ejemplos simples de cómo usar apply con Option and List y explicar por qué debería usarlo en lugar de flatMap ?


Functor es para elevar los cálculos a una categoría.

trait Functor[C[_]] { def map[A, B](f : A => B): C[A] => C[B] }

Y funciona perfectamente para una función de una variable.

val f = (x : Int) => x + 1

Pero para una función de 2 o más, después de levantar a una categoría, tenemos la siguiente firma:

val g = (x: Int) => (y: Int) => x + y Option(5) map g // Option[Int => Int]

Y es la firma de un funtor aplicativo. Y para aplicar el siguiente valor a una función g , se necesita un funtor aplicativo.

trait Applicative[F[_]] { def apply[A, B](f: F[A => B]): F[A] => F[B] }

Y finalmente:

(Applicative[Option] apply (Functor[Option] map g)(Option(5)))(Option(10))

El functor aplicativo es un functor para aplicar un valor especial (valor en categoría) a una función levantada.


Para citarme a mí mismo :

Entonces, ¿por qué molestarse con funcionadores aplicativos en absoluto, cuando tenemos mónadas? En primer lugar, simplemente no es posible proporcionar instancias de mónada para algunas de las abstracciones con las que queremos trabajar: la Validation es el ejemplo perfecto.

En segundo lugar (y relacionado), es solo una práctica de desarrollo sólida utilizar la abstracción menos poderosa que hará el trabajo. En principio, esto puede permitir optimizaciones que de otro modo no serían posibles, pero lo más importante es que hace que el código que escribimos sea más reutilizable.

Para ampliar un poco en el primer párrafo: a veces no puede elegir entre código monádico y aplicativo. Vea el resto de esa respuesta para una discusión sobre por qué es posible que desee usar la Validation de Scalaz (que no tiene y no puede tener una instancia de mónada) para modelar la validación.

Acerca del punto de optimización: probablemente pase un tiempo antes de que esto sea generalmente relevante en Scala o Scalaz, pero consulte, por ejemplo, la documentación de Haskell''s Data.Binary :

El estilo aplicativo a veces puede dar como resultado un código más rápido, ya que el binary intentará optimizar el código agrupando las lecturas.

Escribir un código aplicativo le permite evitar hacer afirmaciones innecesarias sobre dependencias entre cálculos: afirmaciones a las que un código monádico similar lo comprometería. Una biblioteca o compilador suficientemente inteligente podría, en principio, aprovechar este hecho.

Para hacer esta idea un poco más concreta, considere el siguiente código monádico:

case class Foo(s: Symbol, n: Int) val maybeFoo = for { s <- maybeComputeS(whatever) n <- maybeComputeN(whatever) } yield Foo(s, n)

La -comprensión desagua a algo más o menos como el siguiente:

val maybeFoo = maybeComputeS(whatever).flatMap( s => maybeComputeN(whatever).map(n => Foo(s, n)) )

Sabemos que maybeComputeN(whatever) que maybeComputeN(whatever) no depende de s (suponiendo que estos sean métodos de buen comportamiento que no están cambiando algún estado mutable detrás de las escenas), pero el compilador no lo hace: desde su perspectiva, necesita saber s antes puede comenzar a computar n .

La versión aplicativa (usando Scalaz) se ve así:

val maybeFoo = (maybeComputeS(whatever) |@| maybeComputeN(whatever))(Foo(_, _))

Aquí estamos declarando explícitamente que no hay dependencia entre los dos cómputos.

(Y sí, esta |@| sintaxis es bastante horrible; consulte esta publicación en el blog para obtener más información y alternativas).

El último punto es realmente el más importante, sin embargo. Escoger la herramienta menos poderosa que resolverá su problema es un principio tremendamente poderoso. A veces realmente necesitas una composición monádica, en tu método getPhoneByUserId , por ejemplo, pero a menudo no.

Es una pena que tanto Haskell como Scala actualmente hagan que trabajar con mónadas sea mucho más conveniente (sintácticamente, etc.) que trabajar con funtores aplicativos, pero esto es principalmente una cuestión de accidente histórico, y desarrollos como paréntesis idiomáticos son un paso hacia la derecha. dirección.