Jakie są typy lambda w Scali i jakie są ich zalety?

152

Czasami napotykam na pół tajemniczą notację

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

w postach na blogu Scali, które dają mu falę ręczną „użyliśmy tego typu sztuczki lambda”.

Chociaż mam pewne wyobrażenia na ten temat (uzyskujemy anonimowy parametr typu Abez konieczności zanieczyszczania nim definicji?), Nie znalazłem jasnego źródła opisującego, czym jest sztuczka lambda i jakie są jej zalety. Czy to tylko cukier syntaktyczny, czy może otwiera nowe wymiary?

ron
źródło
Zobacz także .
Shelby Moore III

Odpowiedzi:

148

Lambdy typu są niezbędne przez większość czasu, gdy pracujesz z typami wyższego rzędu.

Rozważmy prosty przykład definiowania monady dla prawej projekcji Albo [A, B]. Typeklasa monada wygląda następująco:

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

Teraz Either jest konstruktorem typu z dwoma argumentami, ale aby zaimplementować Monadę, musisz nadać jej konstruktor typu z jednym argumentem. Rozwiązaniem tego problemu jest użycie typu lambda:

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

Oto przykład curry w systemie typów - masz już wybrany typ Either, tak że gdy chcesz utworzyć instancję EitherMonad, musisz określić jeden z typów; drugi oczywiście jest dostarczany w momencie wywołania punktu lub wiązania.

Sztuczka lambda typu wykorzystuje fakt, że pusty blok w pozycji typu tworzy anonimowy typ strukturalny. Następnie używamy składni #, aby uzyskać składową typu.

W niektórych przypadkach możesz potrzebować bardziej wyrafinowanych lambd typu, których pisanie w tekście jest trudne. Oto przykład z mojego dzisiejszego kodu:

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

Ta klasa istnieje wyłącznie po to, że mogę użyć nazwy takiej jak FG [F, G] #IterateeM, aby odnieść się do typu monady IterateeT wyspecjalizowanej w jakiejś wersji transformatorowej drugiej monady, która jest wyspecjalizowana w jakiejś trzeciej monadzie. Kiedy zaczynasz się układać, tego rodzaju konstrukcje stają się bardzo potrzebne. Oczywiście nigdy nie tworzę instancji FG; jest to tylko sztuczka, która pozwala mi wyrazić to, czego chcę w systemie typów.

Kris Nuttycombe
źródło
3
Ciekawe, że Haskell nie obsługuje bezpośrednio lambd na poziomie typu , chociaż niektóre ataki hakerskie nowego typu (np. Biblioteka TypeCompose) mają sposoby na obejście tego.
Dan Burton,
1
Byłbym ciekawy, jak definiujesz bindmetodę dla swojej EitherMonadklasy. :-) Poza tym, jeśli mogę skierować Adriaana na sekundę tutaj, w tym przykładzie nie używasz typów wyższego rzędu. Jesteś w FG, ale nie EitherMonad. Zamiast tego używasz konstruktorów typów , które mają rodzaj * => *. Ten rodzaj jest rzędu 1, który nie jest „wyższy”.
Daniel Śpiewak
2
Myślałem, że *to porządek-1, ale w każdym razie Monada ma (* => *) => *. Zauważysz również, że określiłem „właściwą projekcję Either[A, B]” - implementacja jest trywialna (ale dobre ćwiczenie, jeśli nie robiłeś tego wcześniej!)
Kris Nuttycombe
Wydaje mi się, że argument Daniela, aby nie nazywać *=>*wyższego rzędu, jest uzasadniony analogią, że nie nazywamy zwykłej funkcji (która odwzorowuje niefunkcje na niefunkcje, innymi słowy, zwykłe wartości na zwykłe wartości) funkcją wyższego rzędu.
jhegedus
1
Książka Pierce'a TAPL, strona 442:Type expressions with kinds like (*⇒*)⇒* are called higher-order typeoperators.
jhegedus
52

Korzyści są dokładnie takie same, jak te zapewniane przez funkcje anonimowe.

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

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

Przykładowe użycie w Scalaz 7. Chcemy użyć funkcji, Functorktóra może odwzorować funkcję na drugi element w pliku 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 zapewnia pewne niejawne konwersje, do których można wywnioskować argument typu Functor, więc często całkowicie unikamy ich pisania. Poprzedni wiersz można przepisać jako:

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

Jeśli używasz IntelliJ, możesz włączyć Ustawienia, Styl kodu, Scala, Składanie, Typ Lambdy. To następnie ukrywa crufty części składni i przedstawia bardziej smaczne:

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

Przyszła wersja Scali może bezpośrednio obsługiwać taką składnię.

retronim
źródło
Ten ostatni fragment wygląda naprawdę ładnie. Wtyczka IntelliJ scala z pewnością jest niesamowita!
AndreasScheinert,
1
Dzięki! W ostatnim przykładzie może brakować lambdy. Poza tym, dlaczego funktory krotek zdecydowały się przekształcić ostatnią wartość? Czy jest to konwencja / praktyczny błąd?
ron
1
Prowadzę nightlies dla Niki i nie mam opisanej opcji IDEA. Co ciekawe, nie jest inspekcja dla „Type Applied Lambda mogą zostać uproszczone.”
Randall Schulz
6
Został przeniesiony do Ustawienia -> Edytor -> Składanie kodu.
retronim
@retronym, podczas próby (1, 2).map(a => a + 1)w REPL wystąpił błąd : `` <console>: 11: error: mapowanie wartości nie jest członkiem (Int, Int) (1, 2) .map (a => a + 1) ^ ''
Kevin Meredith,
41

Aby nadać kontekst: ta odpowiedź została pierwotnie opublikowana w innym wątku. Widzisz to tutaj, ponieważ dwa wątki zostały połączone. Pytanie we wspomnianym wątku brzmiało następująco:

Jak rozwiązać tę definicję typu: Pure [({type? [A] = (R, a)}) #?]?

Jakie są powody stosowania takiej konstrukcji?

Snipped pochodzi z biblioteki 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)
  }

//...
}

Odpowiedź:

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

Jeden Pznak podkreślenia w polach po nim oznacza, że ​​jest to konstruktor typu, który przyjmuje jeden typ i zwraca inny typ. Przykłady konstruktorów typu z tego rodzaju: List, Option.

Daj , rodzaj betonu, a to daje , inny rodzaj betonu. Daj i to daje . Itp.ListIntList[Int]ListStringList[String]

Tak więc List, Optionmoże być uznana za funkcje Wysokość Rodzaj liczbę operandów 1. Formalnie mówimy, mają rodzaju * -> *. Gwiazdka oznacza typ.

Teraz Tuple2[_, _]jest konstruktorem typu z rodzajem, (*, *) -> *tzn. Musisz nadać mu dwa typy, aby uzyskać nowy typ.

Ponieważ ich podpisy nie pasują, nie można zastąpić Tuple2za P. To, co musisz zrobić, to częściowo zastosować Tuple2 jeden z jego argumentów, co da nam konstruktor typu z rodzajem * -> *i możemy go zastąpić P.

Niestety Scala nie ma specjalnej składni do częściowego stosowania konstruktorów typów, więc musimy uciekać się do potworności zwanej lambdami typów. (To, co masz w swoim przykładzie). Nazywa się je tak, ponieważ są analogiczne do wyrażeń lambda, które istnieją na poziomie wartości.

Poniższy przykład może pomóc:

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

Edytować:

Więcej równoległych poziomów wartości i typów.

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

W przypadku, który przedstawiłeś, parametr typu Rjest lokalny dla funkcji, Tuple2Purewięc nie możesz go po prostu zdefiniować type PartialTuple2[A] = Tuple2[R, A], ponieważ po prostu nie ma miejsca, w którym możesz umieścić ten synonim.

Aby poradzić sobie z takim przypadkiem, używam następującej sztuczki, która wykorzystuje składowe typu. (Mam nadzieję, że przykład jest oczywisty).

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)]
missingfaktor
źródło
0

type World[M[_]] = M[Int]powoduje, że cokolwiek umieścimy Aw jest zawsze prawdziwe chyba.X[A]implicitly[X[A] =:= Foo[String,Int]]

wiesiu_p
źródło