O que são lambdas de tipo em Scala e quais são seus benefícios?

152

Às vezes eu tropeço na notação semi-misteriosa de

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

nas postagens do blog Scala, que fornecem uma onda manual "usamos o truque lambda de tipo".

Embora eu tenha alguma intuição sobre isso (obtemos um parâmetro de tipo anônimo Asem precisar poluir a definição?), Não encontrei uma fonte clara descrevendo qual é o truque do tipo lambda e quais são seus benefícios. É apenas açúcar sintático, ou abre algumas novas dimensões?

Ron
fonte
Veja também .
Shelby Moore III

Respostas:

148

As lambdas de tipo são vitais um pouco do tempo quando você está trabalhando com tipos de classe superior.

Considere um exemplo simples de definição de uma mônada para a projeção correta de Qualquer um [A, B]. A classe de mônada é assim:

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

Agora, Ou é um construtor de tipos de dois argumentos, mas para implementar o Monad, você precisa atribuir a ele um construtor de tipos de um argumento. A solução para isso é usar um 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 é um exemplo de currying no sistema de tipos - você alterou o tipo de Either, de modo que, ao criar uma instância do EitherMonad, você deve especificar um dos tipos; o outro, é claro, é fornecido no momento em que você chama o ponto ou liga.

O truque lambda de tipo explora o fato de que um bloco vazio em uma posição de tipo cria um tipo estrutural anônimo. Em seguida, usamos a sintaxe # para obter um membro do tipo.

Em alguns casos, você pode precisar de lambdas de tipo mais sofisticado que sejam difíceis de escrever em linha. Aqui está um exemplo do meu código de hoje:

// 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 classe existe exclusivamente para que eu possa usar um nome como FG [F, G] #IterateeM para me referir ao tipo de mônada IterateeT especializada em alguma versão transformadora de uma segunda mônada especializada em outra terceira mônada. Quando você começa a empilhar, esses tipos de construções se tornam muito necessários. Eu nunca instancia um GF, é claro; existe apenas como um hack para me deixar expressar o que eu quero no sistema de tipos.

Kris Nuttycombe
fonte
3
É interessante notar que o Haskell não suporta diretamente lambdas no nível de tipo , embora alguns hackers de tipo novo (por exemplo, a biblioteca TypeCompose) tenham maneiras de contornar isso.
Dan Burton
1
Eu ficaria curioso para ver você definir o bindmétodo para sua EitherMonadclasse. :-) Além disso, se eu puder canalizar Adriaan por um segundo aqui, você não está usando tipos mais altos nesse exemplo. Você está dentro FG, mas não dentro EitherMonad. Em vez disso, você está usando construtores de tipo , que são do tipo * => *. Esse tipo é da ordem 1, que não é "superior".
precisa saber é o seguinte
2
Eu pensei que esse tipo *fosse da ordem 1, mas, de qualquer forma, Monad é gentil (* => *) => *. Além disso, você vai notar que eu especifiquei "a projeção direita Either[A, B]" - (! Mas um bom exercício se você não tiver feito isso antes) a implementação é trivial
Kris Nuttycombe
Acho que o argumento de Daniel de não chamar *=>*mais alto é justificado pela analogia de que não chamamos de função comum (que mapeia não funções para não funções, ou seja, valores simples para valores simples) função de ordem superior.
jhegedus
1
Livro TAPL de Pierce, página 442:Type expressions with kinds like (*⇒*)⇒* are called higher-order typeoperators.
jhegedus
52

Os benefícios são exatamente os mesmos que os conferidos por funções anônimas.

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

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

Um exemplo de uso, com o Scalaz 7. Queremos usar um Functorque possa mapear uma função sobre o segundo elemento em 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)

O Scalaz fornece algumas conversões implícitas que podem inferir o argumento de tipo Functor, portanto, muitas vezes evitamos escrevê-las completamente. A linha anterior pode ser reescrita como:

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

Se você usa o IntelliJ, pode ativar Configurações, Estilo do código, Escala, Dobra, Tipo Lambdas. Isso então oculta as partes cruéis da sintaxe e apresenta as mais palatáveis:

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

Uma versão futura do Scala pode suportar diretamente essa sintaxe.

retrônimo
fonte
Esse último trecho parece muito bom. O plugin IntelliJ scala certamente é incrível!
precisa saber é o seguinte
1
Obrigado! O lambda pode estar ausente no último exemplo. Além disso, por que os functores de tupla escolheram transformar o último valor? É convenção / um padrão prático?
ron
1
Estou correndo todas as noites para Nika e não tenho a opção IDEA descrita. Curiosamente, não é uma inspeção para "Tipo Aplicada Lambda pode ser simplificada."
Randall Schulz
6
Foi movido para Configurações -> Editor -> Dobragem de código.
retronym
@retronym, eu recebi um erro ao tentar (1, 2).map(a => a + 1)no REPL: `<console>: 11: error: value map não é membro de (Int, Int) (1, 2) .map (a => a + 1) ^`
Kevin Meredith
41

Para colocar as coisas em contexto: Esta resposta foi postada originalmente em outro tópico. Você está vendo aqui porque os dois threads foram mesclados. A declaração da pergunta no segmento mencionado foi a seguinte:

Como resolver esta definição de tipo: Pure [({type? [A] = (R, a)}) #?]?

Quais são as razões do uso dessa construção?

Recortado vem da 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)
  }

//...
}

Responda:

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

O sublinhado nas caixas depois Pimplica que é um construtor de tipo pega um tipo e retorna outro tipo. Exemplos de construtores de tipos com este tipo: List, Option.

Listum Int, um tipo concreto, e isso lhe dará List[Int]outro tipo concreto. Dê Listum Stringe dá-lhe List[String]. Etc.

Assim, List, Optionpode ser considerado como funções de nível tipo de aridade 1. Formalmente dizemos, eles têm um tipo * -> *. O asterisco indica um tipo.

Agora Tuple2[_, _]é um construtor de tipos com tipo, (*, *) -> *ou seja, você precisa atribuir dois tipos para obter um novo tipo.

Desde suas assinaturas não corresponderem, você não pode substituir Tuple2para P. O que você precisa fazer é aplicar parcialmente Tuple2 em um de seus argumentos, o que nos dará um construtor de tipo com tipo * -> *, e podemos substituí-lo P.

Infelizmente, Scala não possui sintaxe especial para aplicação parcial de construtores de tipo, e, portanto, temos que recorrer à monstruosidade chamada tipo lambdas. (O que você tem no seu exemplo.) Eles são chamados assim porque são análogos às expressões lambda que existem no nível de valor.

O exemplo a seguir pode ajudar:

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

Mais paralelos em nível de valor e nível 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>

No caso apresentado, o parâmetro type Ré local para funcionar Tuple2Puree, portanto, você não pode simplesmente definir type PartialTuple2[A] = Tuple2[R, A], porque simplesmente não há lugar onde você possa colocar esse sinônimo.

Para lidar com esse caso, use o seguinte truque que faz uso de membros do tipo. (Espero que o exemplo seja auto-explicativo.)

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)]
desaparecido
fonte
0

type World[M[_]] = M[Int]faz com que tudo o que colocamos em Ano X[A]a implicitly[X[A] =:= Foo[String,Int]]sempre é verdade, eu acho.

wiesiu_p
fonte