2012-01-05 4 views
139

Иногда я натыкаюсь в полу-таинственное обозначенияЧто такое лямбды типа Scala и каковы их преимущества?

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

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

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

+0

См. [Также] (https://underscore.io/blog/posts/2016/12/05/type-lambdas.html). –

ответ

137

Тип 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] 
} 

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

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

Это является примером выделки в системе типа - вы кэррите тип Либо, например, что, когда вы хотите создать экземпляр 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 does * not * напрямую поддерживает лямбда-уровни типа] (http://stackoverflow.com/questions/4069840/lambda-for-type-expressions-in-haskell), хотя некоторые хакеры нового типа (например, библиотека TypeCompose) имеет способы обойти это. –

+1

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

+2

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

50

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

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, вы можете включить настройки, Code Style, Scala, складной, Тип Лямбда.Это то hides the crufty parts of the syntax, и представляет более приемлемым:

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

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

+0

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

+1

Спасибо! В последнем примере лямбда может отсутствовать. Кроме того, почему функторы кортежа решили преобразовать последнее значение? Является ли это конвенцией/практическим дефолтом? – ron

+1

Я бегу на ночь для Nika, и у меня нет описанного варианта IDEA. Интересно, что существует такая возможность для проверки «Applied Type Lambda». –

39

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

How to resolve this type definition: Pure[({type ?[a]=(R, a)})#?] ?

What are the reasons of using such construction?

Snipped comes from scalaz library:

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]. И т. Д.

Таким образом, 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]] всегда верно, я думаю.

Смежные вопросы