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


152

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

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

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

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


Ответы:


148

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

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

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


3
Интересно отметить , что Haskell это не напрямую поддерживает лямбды типа на уровне , хотя некоторые Newtype повозка , запряженная волами (например , библиотека TypeCompose) имеет способы рода обойти это.
Дэн Бертон

1
Мне было бы любопытно увидеть, как вы определили bindметод для вашего EitherMonadкласса. :-) Кроме того, если я могу на секунду направить канал Adriaan, вы не будете использовать типы с более высоким родом в этом примере. Вы в FG, но не в EitherMonad. Скорее, вы используете конструкторы типов , которые имеют вид * => *. Этот вид имеет порядок-1, который не «выше».
Даниэль Спивак

2
Я думал, что это *был порядок-1, но в любом случае Монада имеет вид (* => *) => *. Кроме того, вы заметите, что я указал «правильную проекцию Either[A, B]» - реализация тривиальна (но хорошее упражнение, если вы не сделали этого раньше!)
Крис Наттикомб

Я предполагаю, что точка зрения Даниэля о том, что он не вызывает *=>*выше, оправдывается аналогией о том, что мы не называем обычную функцию (которая отображает не функции в не функции, иными словами, простые значения в простые значения) функцию более высокого порядка.
Джегедус

1
Книга Пирса TAPL, стр. 442:Type expressions with kinds like (*⇒*)⇒* are called higher-order typeoperators.
Джегедус

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 может напрямую поддерживать такой синтаксис.


Этот последний фрагмент выглядит действительно красиво. Плагин IntelliJ Scala, безусловно, потрясающий!
AndreasScheinert

1
Спасибо! Лямбда может отсутствовать в последнем примере. Кроме того, почему функторы кортежей решили преобразовать последнее значение? Это соглашение / практическое значение по умолчанию?
Рон

1
Я бегу по ночам для Ника и не описываю опцию IDEA. Интересно, что есть проверка «Лямбда прикладного типа может быть упрощена».
Рэндалл Шульц

6
Он перемещен в Настройки -> Редактор -> Свертывание кода.
Ретроним

@retronym, я получил сообщение об ошибке при попытке (1, 2).map(a => a + 1)в REPL: `<console>: 11: ошибка: карта значений не является членом (Int, Int) (1, 2) .map (a => a + 1) ^`
Кевин Мередит

41

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

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

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

Snipped поставляется из библиотеки скалаза:

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.

Дайте , тип бетона, и это дает вам , еще один конкретный тип. Дайте и это дает вам . И т.п.ListIntList[Int]ListStringList[String]

Таким образом, List, Optionможно рассматривать как функции уровня типа арности 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)]

0

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.