Що таке лямбдаси типу Scala і які їх переваги?


152

Якось я натрапляю на напівзагадкові позначення про

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

у публікаціях блогу Scala, які дають йому "ми використовували цей тип лямбда-фокусу" вручну.

Хоча я маю певну інформацію про це (ми отримуємо параметр анонімного типу Aбез необхідності забруднювати визначення?), Я не знайшов чіткого джерела, що описувало б, що таке хитрість лямбда типу та які його переваги. Це просто синтаксичний цукор чи це відкриває якісь нові виміри?


Дивіться також .
Шелбі Мур III

Відповіді:


148

Ламбда типу життєво важлива небагато часу, коли ви працюєте з вищими типами.

Розглянемо простий приклад визначення монади для правильної проекції Альбо [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 у типовій системі - ви прикрили тип 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, спеціалізованої на деякій версії трансформатора другої монади, яка спеціалізується на якійсь третій монаді. Коли ви починаєте складати, такі конструкції стають дуже потрібними. Звичайно, я ніколи не створюю ФГ; це просто хак, щоб я міг висловити те, що я хочу в системі типів.


3
Цікаво зазначити, що Haskell безпосередньо не підтримує лямбди на рівні типу , хоча деякі хакерські видання нового типу (наприклад, бібліотека TypeCompose) мають способи подолати це.
Ден Бертон

1
Мені буде цікаво побачити, як ви визначаєте bindметод для свого EitherMonadкласу. :-) Окрім цього, якщо я можу на секунду надати канал Адріана, у цьому прикладі ви не використовуєте вищих типів. Ви знаходитесь FG, але не в EitherMonad. Швидше ви використовуєте конструктори типів , які мають вид * => *. Цей вид порядку-1, який не є "вищим".
Даніель Шпієак

2
Я думав, що такий *наказ - 1, але в будь-якому випадку Монада є доброю (* => *) => *. Також ви зауважите, що я вказав "правильну проекцію Either[A, B]" - реалізація є тривіальною (але хороша вправа, якщо ви цього раніше не робили!)
Kris Nuttycombe

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

1
Книга TAPL Пірса, сторінка 442:Type expressions with kinds like (*⇒*)⇒* are called higher-order typeoperators.
jhegedus

52

Переваги точно такі ж, як і анонімні функції.

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

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

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

Scalaz забезпечує деякі неявні перетворення, які можуть зробити висновок аргументу типу Functor, тому ми часто уникаємо їх взагалі писати. Попередній рядок можна переписати як:

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

Якщо ви користуєтеся IntelliJ, ви можете ввімкнути "Налаштування", "Стиль коду", "Скала", "Складка", "Тип лямбда". Потім це приховує грубі частини синтаксису і подає більш приємні:

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

Майбутня версія Scala може безпосередньо підтримувати такий синтаксис.


Цей останній фрагмент виглядає дуже приємно. Плагін IntelliJ scala, безумовно, є приголомшливим!
AndreasScheinert

1
Дякую! В останньому прикладі може бути відсутня лямбда. Крім того, чому кортежі функторів вирішили трансформувати останнє значення? Це конвенція / практичний дефолт?
ron

1
Я запускаю солов’ї для Ніки, і у мене немає описаного варіанту IDEA. Цікаво, що є перевірка на предмет "Прикладного типу лямбда можна спростити".
Рандалл Шульц

6
Він переміщується в Налаштування -> Редактор -> Складання коду.
ретронім

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

41

Щоб поставити речі в контекст: Ця відповідь спочатку була розміщена в іншій темі. Ви бачите його тут, тому що дві нитки були об'єднані. Постановка запитання у зазначеній темі була такою:

Як вирішити таке визначення типу: 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.

Дайте , тип бетону, і це дає вам , ще один конкретний тип. Дайте і це дає вам . І т.д.ListIntList[Int]ListStringList[String]

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

Тепер Tuple2[_, _]це конструктор типу з родом, (*, *) -> *тобто вам потрібно дати йому два типи, щоб отримати новий тип.

Так як їх підпис не збігається, ви не можете замінити Tuple2на P. Що вам потрібно зробити, це частково застосувати Tuple2 один із його аргументів, який надасть нам конструктор типу з видом * -> *, і ми можемо його замінити P.

На жаль, Scala не має спеціального синтаксису для часткового застосування конструкторів типів, і тому нам доводиться вдаватися до чудовисько під назвою лямбда типу. (Що ви маєте у своєму прикладі.) Їх називають тому, що вони аналогічні лямбда-виразам, які існують на рівні значення.

Наступний приклад може допомогти:

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