Quali sono i lambda di tipo in Scala e quali sono i loro benefici?


152

Qualche volta mi imbatto nella notazione semi-misteriosa di

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

nei post sul blog di Scala, che gli danno un'onda "abbiamo usato quel trucco tipo lambda".

Mentre ho qualche intuizione su questo (otteniamo un parametro di tipo anonimo Asenza dover inquinare la definizione con esso?), Non ho trovato alcuna fonte chiara che descriva quale sia il trucco di tipo lambda e quali siano i suoi benefici. È solo zucchero sintattico o apre alcune nuove dimensioni?


Risposte:


148

I lambda di tipo sono vitali per un po 'di tempo quando si lavora con tipi di tipo superiore.

Considera un semplice esempio di definizione di una monade per la giusta proiezione di entrambi [A, B]. La typeclass della monade si presenta così:

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

Ora, O è un costruttore di tipo di due argomenti, ma per implementare Monad, devi dargli un costruttore di tipo di un argomento. La soluzione a questo è usare un tipo 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]
}

Questo è un esempio di curry nel sistema dei tipi: hai inserito il tipo di entrambi, in modo tale che quando vuoi creare un'istanza di EitherMonad, devi specificare uno dei tipi; l'altro ovviamente viene fornito al momento della chiamata o del bind.

Il trucco lambda di tipo sfrutta il fatto che un blocco vuoto in una posizione di tipo crea un tipo strutturale anonimo. Quindi utilizziamo la sintassi # per ottenere un membro del tipo.

In alcuni casi, potresti aver bisogno di lambda di tipo più sofisticato che sono un dolore da scrivere in linea. Ecco un esempio del mio codice di oggi:

// 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] 
}

Questa classe esiste esclusivamente in modo che io possa usare un nome come FG [F, G] #IterateeM per fare riferimento al tipo di monade IterateeT specializzata in qualche versione di trasformatore di una seconda monade specializzata in qualche terza monade. Quando inizi a impilare, questi tipi di costrutti diventano molto necessari. Non ho mai istanziato un FG, ovviamente; è proprio lì come un trucco per farmi esprimere ciò che voglio nel sistema dei tipi.


3
Interessante notare che Haskell non non sostenere direttamente lambda tipo di livello , anche se alcuni aggiustamenti Newtype (ad esempio la biblioteca TypeCompose) ha modi per tipo di aggirare questo.
Dan Burton,

1
Sarei curioso di vederti definire il bindmetodo per la tua EitherMonadclasse. :-) A parte questo, se posso incanalare Adriaan per un secondo qui, non stai usando tipi di tipo superiore in questo esempio. Sei dentro FG, ma non dentro EitherMonad. Piuttosto, stai usando costruttori di tipi , che hanno tipo * => *. Questo tipo è di ordine 1, che non è "superiore".
Daniel Spiewak,

2
Ho pensato che quel tipo *fosse order-1, ma in ogni caso Monad ha kind (* => *) => *. Inoltre, noterai che ho specificato "la giusta proiezione di Either[A, B]" - l'implementazione è banale (ma un buon esercizio se non l'hai mai fatto prima!)
Kris Nuttycombe il

Immagino che il punto di Daniel di non chiamare *=>*più elevato sia giustificato dall'analogia che non chiamiamo una funzione ordinaria (che mappa le non funzioni a non funzioni, in altre parole, valori semplici a valori semplici) funzione di ordine superiore.
jhegedus,

1
Il libro TAPL di Pierce, pagina 442:Type expressions with kinds like (*⇒*)⇒* are called higher-order typeoperators.
jhegedus il

52

I vantaggi sono esattamente gli stessi di quelli conferiti da funzioni anonime.

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

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

Un esempio di utilizzo, con Scalaz 7. Vogliamo usare un Functorche può mappare una funzione sul secondo elemento in a 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 fornisce alcune conversioni implicite a cui inferire l'argomento type Functor, quindi spesso evitiamo di scriverle del tutto. La riga precedente può essere riscritta come:

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

Se si utilizza IntelliJ, è possibile abilitare Impostazioni, Stile codice, Scala, Pieghevole, Tipo Lambdas. Questo quindi nasconde le parti croccanti della sintassi e presenta le più appetibili:

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

Una versione futura di Scala potrebbe supportare direttamente tale sintassi.


L'ultimo frammento sembra davvero carino. Il plugin per scala IntelliJ è sicuramente fantastico!
AndreasScheinert,

1
Grazie! La lambda potrebbe mancare nell'ultimo esempio. A parte, perché i tuple hanno scelto di trasformare l'ultimo valore? È una convenzione / un default pratico?
Ron,

1
Sto gestendo nightly per Nika e non ho descritto l'opzione IDEA. È interessante notare, non v'è un controllo per "Tipo applicata Lambda può essere semplificata."
Randall Schulz,

6
È stato spostato in Impostazioni -> Editor -> Piegatura del codice.
retronym

@retronym, ho riscontrato un errore durante il tentativo (1, 2).map(a => a + 1)in REPL: `<console>: 11: errore: la mappa dei valori non è un membro di (Int, Int) (1, 2) .map (a => a + 1) ^`
Kevin Meredith,

41

Per mettere le cose nel contesto: questa risposta è stata originariamente pubblicata in un'altra discussione. Lo vedi qui perché i due thread sono stati uniti. La dichiarazione della domanda in detto thread era la seguente:

Come risolvere questa definizione di tipo: Pure [({type? [A] = (R, a)}) #?]?

Quali sono i motivi per utilizzare tale costruzione?

Snipped viene dalla libreria 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)
  }

//...
}

Risposta:

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

Il carattere di sottolineatura nelle caselle dopo Pimplica che si tratta di un costruttore di tipo accetta un tipo e ne restituisce un altro. Esempi di costruttori di tipi con questo tipo: List, Option.

Dai Listun Inttipo concreto e ti dà List[Int]un altro tipo concreto. Dai Listun Stringe ti dà List[String]. Eccetera.

Così, List, Optionpossono essere considerate funzioni di livello tipo di arietà 1. Formalmente diciamo, hanno una sorta * -> *. L'asterisco indica un tipo.

Ora Tuple2[_, _]è un costruttore di tipi con kind (*, *) -> *cioè devi dargli due tipi per ottenere un nuovo tipo.

Dal momento che le loro firme non corrispondono, non è possibile sostituire Tuple2per P. Quello che devi fare è applicare parzialmente Tuple2 su uno dei suoi argomenti, che ci darà un costruttore di tipi con kind * -> *, e possiamo sostituirlo P.

Sfortunatamente Scala non ha una sintassi speciale per l'applicazione parziale dei costruttori di tipi, e quindi dobbiamo ricorrere alla mostruosità chiamata tipo lambdas. (Quello che hai nel tuo esempio.) Si chiamano così perché sono analoghi alle espressioni lambda che esistono a livello di valore.

Il seguente esempio potrebbe essere d'aiuto:

// 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>

Modificare:

Più parallelismi a livello di valore e di livello.

// 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>

Nel caso che hai presentato, il parametro type Rè locale per funzionare Tuple2Puree quindi non puoi semplicemente definirlo type PartialTuple2[A] = Tuple2[R, A], perché semplicemente non c'è posto dove puoi mettere quel sinonimo.

Per affrontare un caso del genere, utilizzo il seguente trucco che utilizza membri di tipo. (Speriamo che l'esempio sia autoesplicativo.)

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

0

type World[M[_]] = M[Int]cause che qualunque cosa abbiamo messo in Ain X[A]la implicitly[X[A] =:= Foo[String,Int]]è sempre vero credo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.