2012-01-05 10 views
139

En algún momento me tropiezo en la notación semi-misteriosa de¿Qué son las lambdas tipo en Scala y cuáles son sus beneficios?

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

en Scala entradas de blog, que le dan un "nosotros utilizamos ese tipo lambda-trick" handwave.

Si bien tengo alguna información al respecto (obtenemos un parámetro de tipo anónimo A sin tener que contaminar la definición con él), no encontré una fuente clara que describa el truco tipo lambda y cuáles son sus ventajas. ¿Es solo azúcar sintáctico o abre algunas dimensiones nuevas?

+0

Ver [también] (https://underscore.io/blog/posts/2016/12/05/type-lambdas.html). –

Respuesta

137

Las lambdas de tipo son vitales bastante tiempo cuando trabajas con tipos de mayor nivel.

Considere un ejemplo simple de definición de una mónada para la proyección correcta de Cualquiera [A, B]. La clase de tipos mónada es el siguiente:

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

Ahora, o es un constructor de tipo de dos argumentos, pero para implementar Mónada, es necesario darle un constructor de tipos de un argumento. La solución a esto es utilizar 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] 
} 

Este es un ejemplo de ganarse en el sistema de tipo - que haya al curry el tipo de O, de modo que cuando se quiere crear una instancia de EitherMonad, se tiene que especificar uno de los tipos; el otro, por supuesto, se suministra en el momento en que llamas al punto o en el enlace.

El truco tipo lambda explota el hecho de que un bloque vacío en una posición de tipo crea un tipo estructural anónimo. Luego usamos la sintaxis # para obtener un miembro de tipo.

En algunos casos, es posible que necesite tipos de lambdas más sofisticados que son difíciles de escribir en línea. Aquí está un ejemplo de mi código a partir de hoy:

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

existe esta clase en exclusiva para que pueda utilizar un nombre como FG [F, G] #IterateeM para referirse al tipo de la IterateeT mónada especializada a alguna versión del transformador de una segunda mónada que está especializada en una tercera mónada. Cuando comienzas a apilar, este tipo de construcciones se vuelven muy necesarias. Nunca instanciar un FG, por supuesto; simplemente está ahí como un truco para dejarme expresar lo que quiero en el sistema de tipos.

+3

Es interesante notar que [Haskell * no * admite directamente lambdas de nivel de tipo] (http://stackoverflow.com/questions/4069840/lambda-for-type-expressions-in-haskell), aunque algunos haty de tipo nuevo (p. Ej. la biblioteca TypeCompose) tiene formas de sortear eso. –

+1

Me gustaría verte definir el método 'bind' para tu clase' EitherMonad'. :-) Aparte de eso, si puedo canalizar Adriaan por un segundo aquí, no estás usando tipos de mayor nivel en ese ejemplo. Estás en 'FG', pero no en' EitherMonad'. Por el contrario, está utilizando * constructores tipo *, que tienen tipo '* => *'. Este tipo es de orden 1, que no es "superior". –

+2

Pensé que el tipo '*' era order-1, pero en cualquier caso Monad tiene kind '(* => *) => *'. Además, notarás que especifiqué "la proyección correcta de 'O bien [A, B]' "- la implementación es trivial (¡pero es un buen ejercicio si no lo has hecho antes!) –

50

Los beneficios son exactamente los mismos que aquellos conferidos por funciones anónimas.

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

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

Un ejemplo de uso, con Scalaz 7. Se desea utilizar un Functor que puede mapear una función sobre el segundo elemento en 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 proporciona algunas conversiones implícitas que pueden deducir el argumento de tipo de Functor, lo que a menudo evitar la escritura de éstos por completo. La línea anterior se puede reescribir como:

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

Si utiliza IntelliJ, puede activar la configuración, estilo de código, Scala, plegable, tipo Lambdas.Esto entonces hides the crufty parts of the syntax, y presenta el más apetecible:

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

Una versión futura de Scala podría apoyar directamente a una sintaxis tal.

+0

Ese último fragmento se ve muy bien. ¡El plugin IntelliJ scala seguramente es asombroso! – AndreasScheinert

+1

Gracias! La lambda puede faltar en el último ejemplo. Aparte, ¿por qué los funtores de tuplas eligen transformar el último valor? ¿Es una convención/un incumplimiento práctico? – ron

+1

Estoy ejecutando nightlies para Nika y no tengo la opción IDEA descrita. Curiosamente, existe una inspección para "Tipo de Lambda aplicado que se puede simplificar". –

39

Para poner las cosas en contexto: Esta respuesta se publicó originalmente en otro hilo. Lo está viendo aquí porque los dos hilos se han fusionado. La declaración pregunta en el dicho hilo fue el siguiente:

How to resolve this type definition: Pure[({type ?[a]=(R, a)})#?] ?

What are the reasons of using such construction?

Snipped comes from scalaz library:

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

//... 
} 

Respuesta:

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

El subrayado en las cajas después de P implica que es un tipo constructor toma un tipo y devuelve otro tipo. Ejemplos de constructores de tipo con este tipo: List, Option.

Proporcione List un Int, un tipo concreto, y le da List[Int], otro tipo concreto. Dé List a String y le da List[String]. Etc.

Por lo tanto, List, Option pueden considerarse funciones de nivel de tipo de arity 1. Formalmente decimos que tienen un tipo * -> *. El asterisco denota un tipo.

Ahora Tuple2[_, _] es un constructor de tipo con tipo (*, *) -> * es decir, necesita darle dos tipos para obtener un nuevo tipo.

Dado que sus firmas no coinciden, no puede sustituir Tuple2 por P. Lo que debe hacer es aplicar parcialmenteTuple2 en uno de sus argumentos, lo que nos dará un constructor de tipo con el tipo * -> *, y podemos sustituirlo por P.

Desafortunadamente, Scala no tiene una sintaxis especial para la aplicación parcial de constructores de tipo, por lo que debemos recurrir a la monstruosidad denominada tipo lambdas. (Lo que tienes en tu ejemplo.) Se llaman así porque son análogas a las expresiones lambda que existen a nivel de valor.

El ejemplo siguiente podría ayudar:

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

Editar:

Más nivel de valor y de nivel de tipo paralelos.

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

En el caso de que haya presentado, el parámetro de tipo R es local para funcionar Tuple2Pure y así no se puede simplemente definir type PartialTuple2[A] = Tuple2[R, A], porque simplemente no hay lugar donde se puede poner ese sinónimo.

Para tratar este caso, utilizo el siguiente truco que hace uso de los miembros de tipo. (Afortunadamente el ejemplo es autoexplicativo.)

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] provoca que todo lo que ponemos en A en X[A] la implicitly[X[A] =:= Foo[String,Int]] siempre es cierto, supongo.

Cuestiones relacionadas