Puzzler de syntaxe de fonction dans scalaz

Après avoir regardé la présentation de Nick Partidge sur la dérivation du scalaz , je me suis penché sur cet exemple, qui est tout simplement génial:

import scalaz._ import Scalaz._ def even(x: Int) : Validation[NonEmptyList[Ssortingng], 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)) 

J’essayais de comprendre ce que faisait la méthode , voici le code source:

 def [B](b: M[B])(implicit t: Functor[M], a: Apply[M]): M[(A, B)] = (b, (_: A, _: B)) 

OK, c’est assez déroutant (!) – mais il fait référence à la méthode , qui est déclarée ainsi:

 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) 

J’ai donc quelques questions:

  1. Comment se fait-il que la méthode semble prendre un type de paramètre de type supérieur ( M[B] ) mais peut passer une Validation (qui a deux types de parameters)?
  2. La syntaxe (_: A, _: B) définit la fonction (A, B) => Pair[A,B] attendue par la 2ème méthode: qu’advient-il du Tuple2 / Pair dans le cas de la panne? Il n’y a pas de tuple en vue!

Constructeurs de type en tant que parameters de type

M est un paramètre de type de l’un des principaux pimps de Scalaz, MA , qui représente le constructeur de type (aussi appelé Kinded Type), de la valeur pimped. Ce constructeur de type est utilisé pour rechercher les instances appropriées de Functor et Apply , qui sont des exigences implicites à la méthode < **> .

 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’est-ce qu’un constructeur de types?

De la langue de référence Scala:

Nous distinguons les types de premier ordre et les constructeurs de types, qui prennent les parameters de type et les types de rendement. Un sous-ensemble de types du premier ordre appelé types de valeur représente des ensembles de valeurs (de première classe). Les types de valeur sont soit concrets, soit abstraits. Tout type de valeur concret peut être représenté comme un type de classe, à savoir un désignateur de type (§3.3.3) faisant référence à une classe1 (§5.3), ou un type composé (§3.2.7) représentant une intersection de types, éventuellement avec un raffinement (§3.2.7) qui contraint davantage les types de ses membres. Les types de valeurs abstraits sont introduits par des parameters de type (§4.4) et des liaisons de type abstrait (§4.3). Les parenthèses dans les types sont utilisées pour le regroupement. Nous supposons que les objects et les packages définissent implicitement une classe (du même nom que l’object ou le package, mais inaccessible aux programmes utilisateur).

Les types sans valeur capturent les propriétés des identificateurs qui ne sont pas des valeurs (§3.3). Par exemple, un constructeur de type (§3.3.3) ne spécifie pas directement le type de valeurs. Cependant, lorsqu’un constructeur de type est appliqué aux arguments de type correct, il génère un type de premier ordre, qui peut être un type de valeur. Les types sans valeur sont exprimés indirectement dans Scala. Par exemple, un type de méthode est décrit en écrivant une signature de méthode, qui en soi n’est pas un type réel, bien qu’elle donne lieu à un type de fonction correspondant (§3.3.1). Les constructeurs de types sont un autre exemple, car on peut écrire le type Swap [m [_, _], a, b] = m [b, a], mais il n’y a pas de syntaxe pour écrire directement la fonction de type anonyme correspondante.

List est un constructeur de type. Vous pouvez appliquer le type Int pour obtenir un type de valeur, List[Int] , qui peut classer une valeur. D’autres constructeurs de types prennent plus d’un paramètre.

Le trait scalaz.MA nécessite que son paramètre premier type soit un constructeur de type qui prend un seul type pour renvoyer un type de valeur, avec le trait MA[M[_], A] {} syntaxe trait MA[M[_], A] {} . La définition du paramètre type décrit la forme du constructeur de type, appelé son type. On dit que la liste a le genre ‘ * -> * .

Application partielle des types

Mais comment MA peut-il encapsuler des valeurs de type Validation[X, Y] ? Le type Validation a une sorte (* *) -> * et ne peut être transmis qu’en tant qu’argument de type à un paramètre de type déclaré comme M[_, _] .

Cette conversion implicite dans l’ object Scalaz convertit une valeur de type Validation[X, Y] en 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) } 

Qui à son tour utilise une astuce avec un alias de type dans PartialApply1Of2 pour appliquer partiellement le constructeur de type Validation , en fixant le type des erreurs, mais en laissant le type de succès inappliqué.

PartialApply1Of2[Validation, E]#Apply serait mieux écrit sous la forme [X] => Validation[E, X] . J’ai récemment proposé d’append une telle syntaxe à Scala, cela pourrait arriver en 2.9.

Considérez ceci comme un équivalent au niveau de ce type:

 def validation[A, B](a: A, b: B) = ... def partialApply1Of2[A, BC](f: (A, B) => C, a: A): (B => C) = (b: B) => f(a, b) 

Cela vous permet de combiner la Validation[Ssortingng, Int] avec une Validation[Ssortingng, Boolean] , car les deux partagent le constructeur de type [A] Validation[Ssortingng, A] .

Functeurs Applicatifs

< **> exige que le constructeur de type M ait des instances associées de Apply et Functor . Ceci constitue un foncteur applicatif, qui, comme une monade, est un moyen de structurer un calcul par un effet. Dans ce cas, les sous-calculs peuvent échouer (et, lorsqu’ils le font, nous accumulons les échecs).

Le conteneur Validation[NonEmptyList[Ssortingng], A] peut envelopper une valeur pure de type A dans cet effet. L’opérateur < **> prend deux valeurs effectives et une fonction pure, et les combine avec l’instance de foncteur applicatif pour ce conteneur.

Voici comment cela fonctionne pour le foncteur applicatif Option . L’effet est la possibilité d’un échec.

 val os: Option[Ssortingng] = Some("a") val oi: Option[Int] = Some(2) val result1 = (os < **> oi) { (s: Ssortingng, i: Int) => s * i } assert(result1 == Some("aa")) val result2 = (os < **> (None: Option[Int])) { (s: Ssortingng, i: Int) => s * i } assert(result2 == None) 

Dans les deux cas, il existe une fonction pure de type (Ssortingng, Int) => Ssortingng , appliquée aux arguments effectifs. Notez que le résultat est enveloppé dans le même effet (ou conteneur, si vous préférez), que les arguments.

Vous pouvez utiliser le même modèle sur une multitude de conteneurs dotés d’un foncteur applicatif associé. Toutes les monades sont automatiquement des foncteurs applicatifs, mais il y en a encore plus, comme ZipStream .

Option et [A]Validation[X, A] sont tous deux des Monads, vous pouvez donc également utiliser Bind (aka flatMap):

 val result3 = oi flatMap { i => os map { s => s * i } } val result4 = for {i < - oi; s <- os} yield s * i 

Tupling avec `< | ** |>`

< |**|> est vraiment similaire à < **> , mais il fournit la fonction pure pour vous de simplement construire un Tuple2 à partir des résultats. (_: A, _ B) est un raccourci pour (a: A, b: B) => Tuple2(a, b)

Et au-delà

Voici nos exemples groupés pour Applicative et Validation . J'ai utilisé une syntaxe légèrement différente pour utiliser le foncteur applicatif, (fa ⊛ fb ⊛ fc ⊛ fd) {(a, b, c, d) => .... }

MISE À JOUR: Mais que se passe-t-il dans le cas d'échec?

Qu'advient-il du Tuple2 / Pair dans le cas de défaillance ?

Si l'un des sous-calculs échoue, la fonction fournie n'est jamais exécutée. Il est uniquement exécuté si tous les sous-calculs (dans ce cas, les deux arguments passés à < **> ) ont réussi. Si c'est le cas, il les combine en un Success . Où est cette logique? Ceci définit l'instance Apply pour [A] Validation[X, A] . Nous exigeons que le type X ait un groupe Semigroup disponible, ce qui est la stratégie pour combiner les erreurs individuelles, chacune de type X , en une erreur agrégée du même type. Si vous choisissez Ssortingng comme type d'erreur, le Semigroup[Ssortingng] concatène les chaînes; Si vous choisissez NonEmptyList[Ssortingng] , les erreurs de chaque étape sont concaténées en une liste de erreurs NonEmptyList plus longue. Cette concaténation se produit ci-dessous lorsque deux Failures sont combinés, en utilisant l'opérateur (qui se développe avec des implicits, par exemple, Scalaz.IdentityTo(e1).⊹(e2)(Semigroup.NonEmptyListSemigroup(Semigroup.SsortingngSemigroup)) .

 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) } } 

Monad ou Applicative, comment choisir?

Toujours en train de lire? ( Oui. Ed )

J'ai montré que les sous-calculs basés sur Option ou [A] Validation[E, A] peuvent être combinés avec Apply ou avec Bind . Quand choisirais-tu l'un sur l'autre?

Lorsque vous utilisez Apply , la structure du calcul est fixe. Tous les sous-calculs seront exécutés; les résultats de l'un ne peuvent influencer les autres. Seule la fonction «pure» a un aperçu de ce qui s'est passé. En revanche, les calculs monadiques permettent au premier sous-calcul d’influer sur les derniers.

Si nous utilisions une structure de validation Monadic, le premier échec court-circuiterait l'intégralité de la validation, car il n'y aurait aucune valeur de Success à intégrer à la validation suivante. Cependant, nous sums heureux que les sous-validations soient indépendantes, afin que nous puissions les combiner par le biais de l’application, et collecter tous les échecs rencontrés. La faiblesse des foncteurs applicatifs est devenue une force!