Que sont les lambdas de type dans Scala et quels sont leurs avantages?

152

Parfois, je tombe sur la notation semi-mystérieuse de

def f[T](..) = new T[({type l[A]=SomeType[A,..]})#l] {..} 

dans les articles de blog de Scala, qui lui donnent un «on a utilisé cette astuce de type lambda».

Bien que j'aie une certaine intutition à ce sujet (nous obtenons un paramètre de type anonyme Asans avoir à polluer la définition avec lui?), Je n'ai trouvé aucune source claire décrivant ce qu'est l'astuce de type lambda, et quels sont ses avantages. Est-ce juste du sucre syntaxique ou ouvre-t-il de nouvelles dimensions?

Ron
la source
Voir aussi .
Shelby Moore III

Réponses:

148

Les lambdas de type sont essentiels une bonne partie du temps lorsque vous travaillez avec des types de type supérieur.

Prenons un exemple simple de définition d'une monade pour la projection droite de l'un ou l'autre [A, B]. La classe de types monade ressemble à ceci:

trait Monad[M[_]] {
  def point[A](a: A): M[A]
  def bind[A, B](m: M[A])(f: A => M[B]): M[B]
}

Maintenant, Sither est un constructeur de type de deux arguments, mais pour implémenter Monad, vous devez lui donner un constructeur de type d'un argument. La solution à cela consiste à utiliser un type lambda:

class EitherMonad[A] extends Monad[({type λ[α] = Either[A, α]})#λ] {
  def point[B](b: B): Either[A, B]
  def bind[B, C](m: Either[A, B])(f: B => Either[A, C]): Either[A, C]
}

Ceci est un exemple de curry dans le système de types - vous avez curry le type de l'un ou l'autre, de sorte que lorsque vous voulez créer une instance de EitherMonad, vous devez spécifier l'un des types; l'autre bien sûr est fourni au moment où vous appelez point ou bind.

L'astuce de type lambda exploite le fait qu'un bloc vide dans une position de type crée un type structurel anonyme. Nous utilisons ensuite la syntaxe # pour obtenir un membre de type.

Dans certains cas, vous aurez peut-être besoin de lambdas de type plus sophistiqués qui sont pénibles à écrire en ligne. Voici un exemple de mon code d'aujourd'hui:

// types X and E are defined in an enclosing scope
private[iteratee] class FG[F[_[_], _], G[_]] {
  type FGA[A] = F[G, A]
  type IterateeM[A] = IterateeT[X, E, FGA, A] 
}

Cette classe existe exclusivement pour que je puisse utiliser un nom comme FG [F, G] #IterateeM pour désigner le type de la monade IterateeT spécialisée dans une version de transformateur d'une deuxième monade spécialisée dans une troisième monade. Lorsque vous commencez à empiler, ces types de constructions deviennent très nécessaires. Je n'ai jamais instancié un FG, bien sûr; c'est juste là pour me permettre d'exprimer ce que je veux dans le système de types.

Kris Nuttycombe
la source
3
Il est intéressant de noter que Haskell ne prend pas directement en charge les lambdas au niveau du type , bien que certains hackers newtype (par exemple la bibliothèque TypeCompose) aient des moyens de contourner cela.
Dan Burton
1
Je serais curieux de vous voir définir la bindméthode pour votre EitherMonadclasse. :-) En dehors de cela, si je peux canaliser Adriaan pendant une seconde ici, vous n'utilisez pas de types plus élevés dans cet exemple. Vous êtes dedans FG, mais pas dedans EitherMonad. Vous utilisez plutôt des constructeurs de type , qui ont kind * => *. Ce type est d'ordre 1, ce qui n'est pas "supérieur".
Daniel Spiewak
2
Je pensais que ce genre *était d'ordre 1, mais en tout cas Monad a du genre (* => *) => *. Aussi, vous noterez que j'ai spécifié "la bonne projection de Either[A, B]" - l'implémentation est triviale (mais un bon exercice si vous ne l'avez pas fait auparavant!)
Kris Nuttycombe
Je pense que le point de Daniel de ne pas appeler *=>*plus haut est justifié par l'analogie que nous n'appelons pas une fonction ordinaire (qui mappe des non fonctions à des non fonctions, en d'autres termes, des valeurs simples en valeurs simples) une fonction d'ordre supérieur.
jhegedus
1
Livre TAPL de Pierce, page 442:Type expressions with kinds like (*⇒*)⇒* are called higher-order typeoperators.
jhegedus
52

Les avantages sont exactement les mêmes que ceux conférés par les fonctions anonymes.

def inc(a: Int) = a + 1; List(1, 2, 3).map(inc)

List(1, 2, 3).map(a => a + 1)

Un exemple d'utilisation, avec Scalaz 7. Nous voulons utiliser un Functorqui peut mapper une fonction sur le deuxième élément d'un Tuple2.

type IntTuple[+A]=(Int, A)
Functor[IntTuple].map((1, 2))(a => a + 1)) // (1, 3)

Functor[({type l[a] = (Int, a)})#l].map((1, 2))(a => a + 1)) // (1, 3)

Scalaz fournit des conversions implicites qui peuvent déduire l'argument de type Functor, donc nous évitons souvent de les écrire complètement. La ligne précédente peut être réécrite comme:

(1, 2).map(a => a + 1) // (1, 3)

Si vous utilisez IntelliJ, vous pouvez activer Paramètres, Style de code, Scala, Pliage, Type Lambdas. Cela cache alors les parties cruelles de la syntaxe et présente les plus acceptables:

Functor[[a]=(Int, a)].map((1, 2))(a => a + 1)) // (1, 3)

Une future version de Scala pourrait prendre en charge directement une telle syntaxe.

rétronyme
la source
Ce dernier extrait est vraiment joli. Le plugin IntelliJ scala est sûrement génial!
AndreasScheinert
1
Merci! Le lambda est peut-être manquant dans le dernier exemple. De plus, pourquoi les foncteurs tuple ont-ils choisi de transformer la dernière valeur? Est-ce une convention / un défaut pratique?
ron
1
J'utilise des nightlies pour Nika et je n'ai pas l'option IDEA décrite. Fait intéressant, il y a une inspection pour "Applied Type Lambda peut être simplifié."
Randall Schulz
6
Il est déplacé dans Paramètres -> Éditeur -> Pliage de code.
retronym
@retronym, j'ai eu une erreur en essayant (1, 2).map(a => a + 1)dans REPL: `<console>: 11: error: value map n'est pas membre de (Int, Int) (1, 2) .map (a => a + 1) ^`
Kevin Meredith
41

Pour mettre les choses en contexte: cette réponse a été initialement publiée dans un autre fil de discussion. Vous le voyez ici parce que les deux threads ont été fusionnés. L'énoncé de question dans ledit fil était le suivant:

Comment résoudre cette définition de type: Pure [({type? [A] = (R, a)}) #?]?

Quelles sont les raisons d'utiliser une telle construction?

Snipped provient de la bibliothèque scalaz:

trait Pure[P[_]] {
  def pure[A](a: => A): P[A]
}

object Pure {
  import Scalaz._
//...
  implicit def Tuple2Pure[R: Zero]: Pure[({type ?[a]=(R, a)})#?] = new Pure[({type ?[a]=(R, a)})#?] {
  def pure[A](a: => A) = (Ø, a)
  }

//...
}

Répondre:

trait Pure[P[_]] {
  def pure[A](a: => A): P[A]
}

Le trait de soulignement dans les cases après Pimplique qu'il s'agit d'un constructeur de type qui prend un type et renvoie un autre type. Exemples de constructeurs de types avec ce genre: List, Option.

Donnez Listun Int, un type concret, et cela vous donne List[Int]un autre type concret. Donnez Listun Stringet ça vous donne List[String]. Etc.

Ainsi, List, Optionpeuvent être considérées comme des fonctions de niveau du type d'arité 1. Formellement nous disons, ils ont une sorte * -> *. L'astérisque désigne un type.

Il Tuple2[_, _]s'agit maintenant d' un constructeur de type avec kind, (*, *) -> *c'est-à-dire que vous devez lui donner deux types pour obtenir un nouveau type.

Étant donné que leurs signatures ne correspondent pas, vous ne pouvez pas remplacer Tuple2par P. Ce que vous devez faire est d' appliquer partiellement Tuple2 sur l'un de ses arguments, ce qui nous donnera un constructeur de type avec kind * -> *, et nous pouvons le remplacer P.

Malheureusement, Scala n'a pas de syntaxe spéciale pour l'application partielle des constructeurs de types, et nous devons donc recourir à la monstruosité appelée type lambdas. (Ce que vous avez dans votre exemple.) Ils sont appelés ainsi parce qu'ils sont analogues aux expressions lambda qui existent au niveau de la valeur.

L'exemple suivant peut vous aider:

// VALUE LEVEL

// foo has signature: (String, String) => String
scala> def foo(x: String, y: String): String = x + " " + y
foo: (x: String, y: String)String

// world wants a parameter of type String => String    
scala> def world(f: String => String): String = f("world")
world: (f: String => String)String

// So we use a lambda expression that partially applies foo on one parameter
// to yield a value of type String => String
scala> world(x => foo("hello", x))
res0: String = hello world


// TYPE LEVEL

// Foo has a kind (*, *) -> *
scala> type Foo[A, B] = Map[A, B]
defined type alias Foo

// World wants a parameter of kind * -> *
scala> type World[M[_]] = M[Int]
defined type alias World

// So we use a lambda lambda that partially applies Foo on one parameter
// to yield a type of kind * -> *
scala> type X[A] = World[({ type M[A] = Foo[String, A] })#M]
defined type alias X

// Test the equality of two types. (If this compiles, it means they're equal.)
scala> implicitly[X[Int] =:= Foo[String, Int]]
res2: =:=[X[Int],Foo[String,Int]] = <function1>

Éditer:

Plus de parallèles de niveau de valeur et de niveau de type.

// VALUE LEVEL

// Instead of a lambda, you can define a named function beforehand...
scala> val g: String => String = x => foo("hello", x)
g: String => String = <function1>

// ...and use it.
scala> world(g)
res3: String = hello world

// TYPE LEVEL

// Same applies at type level too.
scala> type G[A] = Foo[String, A]
defined type alias G

scala> implicitly[X =:= Foo[String, Int]]
res5: =:=[X,Foo[String,Int]] = <function1>

scala> type T = World[G]
defined type alias T

scala> implicitly[T =:= Foo[String, Int]]
res6: =:=[T,Foo[String,Int]] = <function1>

Dans le cas que vous avez présenté, le paramètre type Rest local à la fonction Tuple2Pureet vous ne pouvez donc pas simplement définir type PartialTuple2[A] = Tuple2[R, A], car il n'y a tout simplement aucun endroit où vous pouvez mettre ce synonyme.

Pour faire face à un tel cas, j'utilise l'astuce suivante qui utilise des membres de type. (J'espère que l'exemple est explicite.)

scala> type Partial2[F[_, _], A] = {
     |   type Get[B] = F[A, B]
     | }
defined type alias Partial2

scala> implicit def Tuple2Pure[R]: Pure[Partial2[Tuple2, R]#Get] = sys.error("")
Tuple2Pure: [R]=> Pure[[B](R, B)]
manquant
la source
0

type World[M[_]] = M[Int]les causes que tout ce que nous mettons dans Adans X[A]l' implicitly[X[A] =:= Foo[String,Int]]est toujours vrai guess I.

wiesiu_p
la source