Rompecabezas de sintaxis de funciones en scalaz
functional-programming scala-2.8 (1)
Escriba constructores como parámetros de tipo
M
es un parámetro de tipo para uno de los proxenetas principales de Scalaz, MA , que representa el Constructor de Tipo (también conocido como Tipo Mayor Vinculado) del valor de proxeneta. Este constructor de tipo se usa para buscar las instancias apropiadas de Functor
y Apply
, que son requisitos implícitos para el método <**>
.
trait MA[M[_], A] {
val value: M[A]
def <**>[B, C](b: M[B], z: (A, B) => C)(implicit t: Functor[M], a: Apply[M]): M[C] = ...
}
¿Qué es un Constructor de Tipo?
De la referencia del lenguaje de Scala:
Distinguimos entre tipos de primer orden y constructores de tipos, que toman tipos de parámetros y tipos de rendimiento. Un subconjunto de tipos de primer orden llamados tipos de valores representa conjuntos de valores (de primera clase). Los tipos de valores son concretos o abstractos. Cada tipo de valor concreto puede representarse como un tipo de clase, es decir, un designador de tipo (§3.2.3) que se refiere a una clase1 (§5.3), o como un tipo compuesto (§3.2.7) que representa una intersección de tipos, posiblemente con un refinamiento (§3.2.7) que restringe aún más los tipos de sus miembros. Los tipos de valores abstractos se introducen mediante parámetros de tipo (§4.4) y enlaces de tipo abstracto (§4.3). Los paréntesis en los tipos se utilizan para agrupar. Suponemos que los objetos y paquetes también definen implícitamente una clase (del mismo nombre que el objeto o paquete, pero inaccesible para los programas de usuario).
Los tipos sin valor capturan propiedades de identificadores que no son valores (§3.3). Por ejemplo, un constructor de tipos (§3.3.3) no especifica directamente el tipo de valores. Sin embargo, cuando se aplica un constructor de tipos a los argumentos de tipo correctos, produce un tipo de primer orden, que puede ser un tipo de valor. Los tipos sin valor se expresan indirectamente en Scala. Por ejemplo, un tipo de método se describe anotando una firma de método, que en sí misma no es un tipo real, aunque da lugar a un tipo de función correspondiente (§3.3.1). Los constructores de tipo son otro ejemplo, ya que uno puede escribir el tipo Swap [m [_, _], a, b] = m [b, a], pero no hay sintaxis para escribir la función de tipo anónimo correspondiente directamente.
List
es un constructor de tipos. Puede aplicar el tipo Int
para obtener un Tipo de valor, List[Int]
, que puede clasificar un valor. Otros constructores de tipos toman más de un parámetro.
El rasgo scalaz.MA
requiere que su primer parámetro de tipo sea un constructor de tipo que tome un solo tipo para devolver un tipo de valor, con el trait MA[M[_], A] {}
sintaxis trait MA[M[_], A] {}
. La definición de parámetro de tipo describe la forma del constructor de tipo, que se conoce como su clase. Se dice que la lista tiene el tipo '' * -> *
.
Aplicación parcial de tipos
Pero, ¿cómo puede MA
ajustar valores de tipo Validation[X, Y]
? El tipo Validation
tiene un tipo (* *) -> *
, y solo se puede pasar como un argumento de tipo a un parámetro de tipo declarado como M[_, _]
.
Esta conversión implícita en el objeto Scalaz convierte un valor de tipo Validation[X, Y]
en un MA
:
object Scalaz {
implicit def ValidationMA[A, E](v: Validation[E, A]): MA[PartialApply1Of2[Validation, E]#Apply, A] = ma[PartialApply1Of2[Validation, E]#Apply, A](v)
}
Que a su vez usa un truco con un alias de tipo en PartialApply1Of2 para aplicar parcialmente la Validation
constructor de tipo, arreglando el tipo de error, pero dejando el tipo de éxito sin aplicar.
PartialApply1Of2[Validation, E]#Apply
sería mejor escrito como [X] => Validation[E, X]
. Hace poco propuse agregar tal sintaxis a Scala, podría suceder en 2.9.
Piense en esto como un equivalente de nivel de tipo de esto:
def validation[A, B](a: A, b: B) = ...
def partialApply1Of2[A, B C](f: (A, B) => C, a: A): (B => C) = (b: B) => f(a, b)
Esto le permite combinar Validation[String, Int]
con Validation[String, Boolean]
, porque ambos comparten el constructor de tipo [A] Validation[String, A]
.
Funcionantes Aplicativos
<**>
exige que el constructor de tipos M
tenga instancias asociadas de Apply y Functor . Esto constituye un Funcionador Aplicativo, que, como una Mónada, es una forma de estructurar un cálculo a través de algún efecto. En este caso, el efecto es que los subcomputadores pueden fallar (y cuando lo hacen, acumulamos los fallos).
La Validation[NonEmptyList[String], A]
contenedor Validation[NonEmptyList[String], A]
puede ajustar un valor puro de tipo A
en este ''efecto''. El operador <**>
toma dos valores efectivos, y una función pura, y los combina con la instancia del Funcionador Aplicativo para ese contenedor.
Así es como funciona para el functor aplicativo de Option
. El ''efecto'' aquí es la posibilidad de fracaso.
val os: Option[String] = Some("a")
val oi: Option[Int] = Some(2)
val result1 = (os <**> oi) { (s: String, i: Int) => s * i }
assert(result1 == Some("aa"))
val result2 = (os <**> (None: Option[Int])) { (s: String, i: Int) => s * i }
assert(result2 == None)
En ambos casos, hay una función pura de tipo (String, Int) => String
, que se aplica a los argumentos efectivos. Observe que el resultado está envuelto en el mismo efecto (o contenedor, si lo desea), como los argumentos.
Puede usar el mismo patrón en una gran cantidad de contenedores que tengan un Funcionador de aplicación asociado. Todas las mónadas son funcionantes aplicativos automáticamente, pero hay incluso más, como ZipStream
.
Option
y la [A]Validation[X, A]
son ambas Mónadas, por lo que también podría usarse Bind
(también conocido como flatMap):
val result3 = oi flatMap { i => os map { s => s * i } }
val result4 = for {i <- oi; s <- os} yield s * i
Tupling con `<| ** |>`
<|**|>
es muy similar a <**>
, pero proporciona la función pura para que simplemente cree un Tuple2 a partir de los resultados. (_: A, _ B)
es una abreviatura de (a: A, b: B) => Tuple2(a, b)
Y más allá
Aquí están nuestros ejemplos incluidos para Applicative and Validation . Utilicé una sintaxis ligeramente diferente para usar el Funcionador Aplicativo, (fa ⊛ fb ⊛ fc ⊛ fd) {(a, b, c, d) => .... }
ACTUALIZACIÓN: ¿Pero qué sucede en el caso de falla?
¿Qué le está pasando a Tuple2 / Pair en el caso de falla ?
Si falla alguno de los subcomputadores, la función proporcionada nunca se ejecuta. Solo se ejecuta si todos los subcomputaciones (en este caso, los dos argumentos pasados a <**>
) son exitosos. Si es así, combina esto en un Success
. ¿Dónde está esta lógica? Esto define la instancia de Apply
para [A] Validation[X, A]
. Requerimos que el tipo X debe tener un Semigroup
disponible, que es la estrategia para combinar los errores individuales, cada uno de tipo X
, en un error agregado del mismo tipo. Si elige String
como su tipo de error, Semigroup[String]
concatena las cadenas; si elige NonEmptyList[String]
, los errores de cada paso se concatenan en una NonEmptyList
de errores más NonEmptyList
. Esta concatenación ocurre a continuación cuando se combinan dos Failures
, utilizando el operador ((que se expande con implícitos a, por ejemplo, Scalaz.IdentityTo(e1).⊹(e2)(Semigroup.NonEmptyListSemigroup(Semigroup.StringSemigroup))
.
implicit def ValidationApply[X: Semigroup]: Apply[PartialApply1Of2[Validation, X]#Apply] = new Apply[PartialApply1Of2[Validation, X]#Apply] {
def apply[A, B](f: Validation[X, A => B], a: Validation[X, A]) = (f, a) match {
case (Success(f), Success(a)) => success(f(a))
case (Success(_), Failure(e)) => failure(e)
case (Failure(e), Success(_)) => failure(e)
case (Failure(e1), Failure(e2)) => failure(e1 ⊹ e2)
}
}
Mónada o Aplicativo, ¿cómo debo elegir?
¿Seguir leyendo? ( Sí. Ed )
He demostrado que los subcomputadores basados en la Option
o [A] Validation[E, A]
pueden combinarse con Apply
o con Bind
. ¿Cuándo elegirías uno sobre el otro?
Cuando usa Apply
, la estructura del cálculo es fija. Todos los subcomputamientos se ejecutarán; los resultados de uno no pueden influir en los otros. Solo la función ''pura'' tiene una visión general de lo que sucedió. Los cómputos monádicos, por otro lado, permiten que el primer sub-cálculo influya en los posteriores.
Si utilizáramos una estructura de validación Monadic, la primera falla pondría en cortocircuito toda la validación, ya que no habría valor de Success
para alimentar la validación subsiguiente. Sin embargo, nos complace que las subvalidaciones sean independientes, por lo que podemos combinarlas a través del Aplicativo y recopilar todas las fallas que encontramos. ¡La debilidad de los Funcionadores Aplicativos se ha convertido en una fortaleza!
Después de ver la presentación de Nick Partidge sobre la obtención de scalaz , llegué a mirar este ejemplo, que es simplemente increíble:
import scalaz._
import Scalaz._
def even(x: Int) : Validation[NonEmptyList[String], Int]
= if (x % 2 ==0) x.success else "not even: %d".format(x).wrapNel.fail
println( even(3) <|*|> even(5) ) //prints: Failure(NonEmptyList(not even: 3, not even: 5))
Estaba tratando de entender qué estaba haciendo el método <|*|>
, aquí está el código fuente:
def <|*|>[B](b: M[B])(implicit t: Functor[M], a: Apply[M]): M[(A, B)]
= <**>(b, (_: A, _: B))
OK, eso es bastante confuso (!) - pero hace referencia al método <**>
, que se declara así:
def <**>[B, C](b: M[B], z: (A, B) => C)(implicit t: Functor[M], a: Apply[M]): M[C]
= a(t.fmap(value, z.curried), b)
Entonces tengo algunas preguntas:
- ¿Cómo es que el método parece tomar un tipo de parámetro de un tipo más alto (
M[B]
) pero puede pasar unaValidation
(que tiene dos parámetros de tipo)? - La sintaxis
(_: A, _: B)
define la función(A, B) => Pair[A,B]
que el segundo método espera: ¿qué está sucediendo con el Tuple2 / Pair en el caso de falla? ¡No hay una tupla a la vista!