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

139

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

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

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

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

  • 0
    Смотрите также
Теги:
types

4 ответа

131
Лучший ответ

Тип lambdas очень важен, когда вы работаете с более высокоподобными типами.

Рассмотрим простой пример определения монады для правой проекции Либо [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]
}

Это пример currying в системе типов - вы указали тип Либо, так что, когда вы хотите создать экземпляр 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. это просто как хак, чтобы я мог выразить то, что хочу в системе типов.

  • 3
    Интересно отметить, что Haskell напрямую не поддерживает лямбды на уровне типов , хотя некоторые хакеры нового типа (например, библиотека TypeCompose) имеют способы обойти это.
  • 1
    Мне было бы любопытно увидеть, как вы определяете метод bind для вашего класса EitherMonad . :-) Кроме того, если я могу на секунду направить канал Adriaan, вы не будете использовать типы с более высоким родом в этом примере. Вы в FG , но не в EitherMonad . Скорее, вы используете конструкторы типов , которые имеют вид * => * . Этот вид имеет порядок-1, который не "выше".
Показать ещё 3 комментария
52

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

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, Складывать, Тип Lambdas. Таким образом, скрывает крутые части синтаксиса и представляет более приятные:

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

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

  • 0
    Этот последний фрагмент выглядит действительно красиво. Плагин IntelliJ Scala, безусловно, потрясающий!
  • 1
    Спасибо! Лямбда может отсутствовать в последнем примере. Кроме того, почему функторы кортежей решили преобразовать последнее значение? Это соглашение / практическое значение по умолчанию?
Показать ещё 5 комментариев
39

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

Как разрешить определение этого типа: 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 a Int, конкретный тип, и он даст вам List[Int] другой конкретный тип. Дайте List a String, и он даст вам List[String]. Etc.

Итак, 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>

Edit:

Дополнительные уровни уровня и уровня уровня.

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню