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

152

En algún momento me encuentro 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 "utilizamos ese truco tipo lambda".

Si bien tengo algo de intuición sobre esto (¿obtenemos un parámetro de tipo anónimo Asin tener que contaminar la definición con él?), No encontré una fuente clara que describa cuál es el truco de lambda de tipo y cuáles son sus beneficios. ¿Es solo azúcar sintáctico o abre algunas dimensiones nuevas?

ron
fuente
Ver también .
Shelby Moore III

Respuestas:

148

Las lambdas de tipo son vitales bastante tiempo cuando trabajas con tipos de clase superior.

Considere un ejemplo simple de definir una mónada para la proyección correcta de Cualquiera [A, B]. La clase de mónada 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, Oither es un constructor de tipo de dos argumentos, pero para implementar Monad, debe darle un constructor de tipo de un argumento. La solución a 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 curry en el sistema de tipos: ha cursado el tipo de Either, de modo que cuando desea crear una instancia de EitherMonad, debe especificar uno de los tipos; el otro, por supuesto, se proporciona al momento de llamar al punto o al enlace.

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 sofisticado que es difícil escribir en línea. Aquí hay 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 referirme al tipo de la mónada IterateeT especializada en alguna versión transformadora de una segunda mónada que está especializada en alguna tercera mónada. Cuando comienzas a apilar, este tipo de construcciones se vuelven muy necesarias. Nunca instanciaré un FG, por supuesto; solo está ahí como un truco para dejarme expresar lo que quiero en el sistema de tipos.

Kris Nuttycombe
fuente
3
Es interesante observar que Haskell no admite directamente lambdas de nivel de tipo , aunque algunos hackers de tipo nuevo (por ejemplo, la biblioteca TypeCompose) tienen formas de solucionarlo.
Dan Burton el
1
Me gustaría ver cómo define el bindmétodo para su EitherMonadclase. :-) Aparte de eso, si puedo canalizar a Adriaan por un segundo aquí, no estás usando tipos de tipo superior en ese ejemplo. Estás dentro FG, pero no dentro EitherMonad. Por el contrario, está utilizando constructores de tipos , que tienen kind * => *. Este tipo es de orden 1, que no es "superior".
Daniel Spiewak
2
Pensé que ese tipo *era la orden 1, pero en cualquier caso Monad tiene el tipo (* => *) => *. Además, notará que especifiqué "la proyección correcta de Either[A, B]" - la implementación es trivial (¡pero un buen ejercicio si no lo ha hecho antes!)
Kris Nuttycombe
Supongo que el punto de Daniel no llama *=>* más alto se justifica por la analogía de que no llamamos a una función ordinaria (que asigna funciones no funciones a funciones no, en otras palabras, valores simples a valores simples) función de orden superior.
jhegedus
1
Libro TAPL de Pierce, página 442: Type expressions with kinds like (*⇒*)⇒* are called higher-order typeoperators.
jhegedus
52

Los beneficios son exactamente los mismos que los que confieren las 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 Functorque pueda mapear una función sobre el segundo elemento en 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 proporciona algunas conversiones implícitas que pueden inferir el argumento de tipo 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 usa IntelliJ, puede habilitar Configuración, Estilo de código, Scala, Plegado, Tipo Lambdas. Esto luego oculta las partes crudas de la sintaxis y presenta las más sabrosas:

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

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

retrónimo
fuente
Ese último fragmento se ve muy bien. ¡IntelliJ scala plugin seguramente es increíble!
AndreasScheinert el
1
¡Gracias! La lambda puede faltar en el último ejemplo. Además, ¿por qué los funerarios de tuplas eligieron transformar el último valor? ¿Es una convención / un defecto práctico?
ron
1
Estoy ejecutando nightlies para Nika y no tengo la opción IDEA descrita. Curiosamente, no es una inspección de "Applied Tipo Lambda se puede simplificar."
Randall Schulz
66
Se mueve a Configuración -> Editor -> Plegado de código.
retrónimo
@retronym, recibí un error al intentar (1, 2).map(a => a + 1)en REPL: `<console>: 11: error: value map no es miembro de (Int, Int) (1, 2) .map (a => a + 1) ^`
Kevin Meredith
41

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 de preguntas en dicho hilo fue la siguiente:

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

¿Cuáles son las razones de usar tal construcción?

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

//...
}

Responder:

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

El subrayado en los cuadros siguientes Pimplica que es un constructor de tipos que toma un tipo y devuelve otro tipo. Ejemplos de constructores de tipos con este tipo: List,Option .

Dale Listun Inttipo concreto, y te da List[Int]otro tipo concreto. Da Listun Stringy te daList[String] . Etc.

Así que, List, Optionpuede ser considerado como funciones de nivel de tipo de aridad 1. Formalmente se dice, tienen una especie* -> * . El asterisco denota un tipo.

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

Ya que sus firmas no coinciden, no se puede sustituir Tuple2por P. Lo que debe hacer es aplicar parcialmente Tuple2 uno de sus argumentos, lo que nos dará un constructor de tipo con kind * -> *, y podemos sustituirlo porP .

Desafortunadamente, Scala no tiene una sintaxis especial para la aplicación parcial de constructores de tipos, por lo que debemos recurrir a la monstruosidad llamada tipo lambdas. (Lo que tiene en su ejemplo.) Se llaman así porque son análogos 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 nivel de valor y paralelos de 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 haya presentado, el parámetro tipo Res local para funcionar Tuple2Purey, por lo tanto, no puede simplemente definirtype PartialTuple2[A] = Tuple2[R, A] , porque simplemente no hay lugar donde pueda poner ese sinónimo.

Para tratar un caso así, utilizo el siguiente truco que hace uso de los 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)]
missingfaktor
fuente
0

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

wiesiu_p
fuente