¿Qué son las lambdas tipo en Scala y cuáles son sus beneficios?


En algún momento me tropiezo con la notación semi-misteriosa de

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

En las publicaciones de blog de Scala, que le dan una onda manual de "usamos ese truco de tipo lambda".

Mientras tengo alguna intuición sobre esto (obtenemos un parámetro de tipo anónimo A sin tener que contaminar la definición con él?), no encontré ninguna fuente clara que describa cuál es el truco de tipo lambda, y cuáles son sus beneficios. ¿Es solo azúcar sintáctica, o abre nuevas dimensiones?

 140
Author: Electric Coffee, 2012-01-05

4 answers

Las lambdas de tipo son vitales bastante tiempo cuando se trabaja con tipos de tipo superior.

Considere un ejemplo simple de definir una mónada para la proyección correcta de[A, B]. La clase monad se ve así:

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 bien es un constructor de tipo de dos argumentos, pero para implementar Monad, es necesario darle un constructor de tipo de un argumento. La solución para esto es usar 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 currying en el tipo sistema-usted ha curried el tipo de Cualquiera, tal que cuando usted quiere crear una instancia de EitherMonad, usted tiene que especificar uno de los tipos; el otro por supuesto se suministra en el momento de llamar a point o bind.

El truco de 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 lambdas de tipo más sofisticadas que son un dolor para escribir en línea. He aquí un ejemplo de mi código 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] 
}

Esta clase existe exclusivamente para que pueda usar un nombre como FG[F, G]#IterateeM para referirse al tipo de la mónada IterateeT especializada a alguna versión transformadora de una segunda mónada que está especializada a alguna tercera mónada. Cuando empiezas a apilar, este tipo de construcciones se vuelven muy necesarias. Nunca instancio un FG, por supuesto; solo está ahí como un truco para dejarme expresar lo que quiero en el sistema de tipos.

 141
Author: Kris Nuttycombe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-01-05 01:08:59

Los beneficios son exactamente los mismos que los 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. Queremos usar un Functor que pueda 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 inferir el argumento type a Functor, por lo que a menudo evitamos escribirlas por completo. La línea anterior se puede reescribir como:

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

Si utiliza IntelliJ, puede habilitar Ajustes, Estilo de código, Scala, Plegado, Escribe Lambdas. Esto entonces oculta las partes crufty de la sintaxis, y presenta la más apetecible:

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

Una futura versión de Scala podría soportar directamente dicha sintaxis.

 50
Author: retronym,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-01-05 04:23:38

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

Cómo resolver esta definición de tipo: Pure [({type ?[a]=(R, a)})#?] ?

¿Cuáles son las razones para utilizar tal construcción?

Snipped viene de la biblioteca 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)
  }

//...
}

Respuesta:

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

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

Da List un Int, un tipo concreto, y te da List[Int], otro tipo concreto. Da List a String y te da List[String]. Sucesivamente.

So, List, Option se puede considerar que son funciones de nivel de tipo de arity 1. Formalmente decimos, tienen un tipo * -> *. El asterisco indica una 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 se puede sustituir Tuple2 por P. Lo que necesita hacer es aplicar parcialmente Tuple2 en uno de sus argumentos, que nos dará un constructor de tipo con tipo * -> *, y podemos sustituirlo por P.

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

El siguiente ejemplo 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 paralelos de nivel de valor y nivel de tipo.

// 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 que ha presentado, el parámetro type R es local para la función Tuple2Pure y, por lo tanto, no puede definir simplemente type PartialTuple2[A] = Tuple2[R, A], porque simplemente no hay lugar donde puedes poner ese sinónimo.

Para lidiar con tal caso, utilizo el siguiente truco que hace uso de miembros de tipo. (Esperemos que el ejemplo se explique por sí mismo.)

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)]
 39
Author: missingfaktor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-18 05:59:28

type World[M[_]] = M[Int] causas que todo lo que ponemos en A en X[A] el implicitly[X[A] =:= Foo[String,Int]] siempre es cierto, supongo.

 0
Author: user143382,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-02 10:05:54