Какие лямбды типа в Scala и каковы их преимущества?

Когда-нибудь я натыкаюсь на полу-таинственную нотацию

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

в сообщениях в блоге Scala, в которых говорится, что мы использовали эту "лямбда-трюк".

Хотя у меня есть интуиция по этому поводу (мы получаем параметр анонимного типа A без необходимости загрязнять определение этим?), я не нашел четкого источника, описывающего, что такое лямбда-трюк типа, и каковы его преимущества. Это просто синтаксический сахар или он открывает новые измерения?

4 ответа

Решение

Лямбды типа жизненно важны в то время, когда вы работаете с типами с более высоким родом.

Рассмотрим простой пример определения монады для правильной проекции Either[A, B]. Класс типов монады выглядит так:

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

Теперь, любой из них является конструктором типа с двумя аргументами, но для реализации Monad вам нужно дать ему конструктор типа с одним аргументом. Решением этого является использование типа лямбда:

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

Это пример карри в системе типов - вы каррировали тип Either, так что, когда вы хотите создать экземпляр EitherMonad, вы должны указать один из типов; другой, конечно, предоставляется в тот момент, когда вы звоните или связываете.

Лямбда-трюк типа использует тот факт, что пустой блок в позиции типа создает анонимный структурный тип. Затем мы используем синтаксис # для получения члена типа.

В некоторых случаях вам могут понадобиться более сложные лямбды типа, которые сложно выписать в строке. Вот пример из моего сегодняшнего кода:

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

Этот класс существует исключительно для того, чтобы я мог использовать имя типа FG[F, G]#IterateeM для ссылки на тип монады IterateeT, специализированной для некоторой трансформаторной версии второй монады, которая специализирована для некоторой третьей монады. Когда вы начинаете складывать, эти виды конструкций становятся очень необходимыми. Конечно, я никогда не запускаю FG; это просто как хак, чтобы я мог выразить то, что я хочу, в системе типов.

Преимущества точно такие же, как и у анонимных функций.

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

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

Пример использования с Scalaz 7. Мы хотим использовать Functor который может отобразить функцию над вторым элементом в 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 предоставляет некоторые неявные преобразования, которые могут выводить аргумент типа Functorпоэтому мы часто избегаем писать их вообще. Предыдущая строка может быть переписана как:

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

Если вы используете IntelliJ, вы можете включить Настройки, Стиль кода, Scala, Складывание, Тип лямбды. Это тогда скрывает грубые части синтаксиса и представляет более приемлемый:

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

Будущая версия Scala может напрямую поддерживать такой синтаксис.

Чтобы поместить вещи в контекст: Этот ответ был первоначально размещен в другой теме. Вы видите это здесь, потому что два потока были объединены. Постановка вопроса в указанной ветке была следующей:

Как разрешить определение этого типа: Pure[({type?[A]=(R, a)})#?]?

Каковы причины использования такой конструкции?

Snipped поставляется из библиотеки 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)
  }

//...
}

Ответ:

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

Подчеркивание в коробках после P подразумевает, что это конструктор типов, принимает один тип и возвращает другой тип. Примеры конструкторов типов с этим видом: List, Option,

Дать List Int, конкретный тип, и это дает вам List[Int]другой конкретный тип. Дать List String и это дает вам List[String], И т.п.

Так, List, Option можно считать функциями уровня типа arity 1. Формально мы говорим, что они имеют вид * -> *, Звездочка обозначает тип.

Сейчас Tuple2[_, _] это конструктор типа с видом (*, *) -> * т.е. вам нужно дать ему два типа, чтобы получить новый тип.

Поскольку их подписи не совпадают, вы не можете заменить Tuple2 за P, То, что вам нужно сделать, это частично применить Tuple2 на одном из его аргументов, который даст нам конструктор типа с видом * -> *и мы можем заменить его на P,

К сожалению, в Scala нет специального синтаксиса для частичного применения конструкторов типов, и поэтому нам приходится прибегать к чудовищу, называемому типом lambdas. (Что у вас есть в вашем примере.) Они называются так, потому что они аналогичны лямбда-выражениям, существующим на уровне значений.

Следующий пример может помочь:

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

Редактировать:

Больше параллелей между уровнем значения и уровнем типа.

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

В представленном вами случае параметр типа R является локальным для функционирования Tuple2Pure и поэтому вы не можете просто определить type PartialTuple2[A] = Tuple2[R, A]потому что просто нет места, где можно поставить этот синоним.

Чтобы справиться с таким случаем, я использую следующую уловку, которая использует члены типа. (Надеюсь, пример не требует пояснений.)

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

type World[M[_]] = M[Int] вызывает то, что мы вкладываем в A в X[A] implicitly[X[A] =:= Foo[String,Int]] это всегда правда, я думаю.

Другие вопросы по тегам