Loại lambdas trong Scala là gì và lợi ích của chúng là gì?


152

Thỉnh thoảng tôi vấp vào ký hiệu bán bí ẩn

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

trong các bài đăng trên blog của Scala, nó cung cấp cho nó một bản handwave "chúng tôi đã sử dụng thủ thuật kiểu lambda" đó.

Mặc dù tôi có một số ý kiến ​​về điều này (chúng tôi có được một tham số loại ẩn danh Amà không phải làm ô nhiễm định nghĩa với nó?), Tôi không tìm thấy nguồn rõ ràng nào mô tả thủ thuật lambda là gì và lợi ích của nó là gì. Nó chỉ là cú pháp đường, hay nó mở ra một số chiều mới?


Câu trả lời:


148

Loại lambdas rất quan trọng một chút thời gian khi bạn đang làm việc với các loại loại cao hơn.

Hãy xem xét một ví dụ đơn giản về việc xác định một đơn nguyên cho phép chiếu đúng của [A, B]. Kiểu chữ đơn nguyên trông như thế này:

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

Bây giờ, Hoặc là một hàm tạo kiểu gồm hai đối số, nhưng để triển khai Monad, bạn cần cung cấp cho nó một hàm tạo kiểu của một đối số. Giải pháp cho vấn đề này là sử dụng loại 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]
}

Đây là một ví dụ về currying trong hệ thống loại - bạn đã chọn loại Either, sao cho khi bạn muốn tạo một thể hiện của EitherMonad, bạn phải chỉ định một trong các loại; cái khác tất nhiên được cung cấp tại thời điểm bạn gọi điểm hoặc liên kết.

Thủ thuật lambda loại khai thác thực tế là một khối trống ở vị trí loại tạo ra một loại cấu trúc ẩn danh. Sau đó chúng tôi sử dụng cú pháp # để có được một thành viên loại.

Trong một số trường hợp, bạn có thể cần loại lambdas tinh vi hơn là một nỗi đau để viết ra nội tuyến. Đây là một ví dụ từ mã của tôi từ hôm nay:

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

Lớp này tồn tại riêng để tôi có thể sử dụng một tên như FG [F, G] #IterateeM để chỉ loại đơn vị IterateeT chuyên dùng cho một số phiên bản biến áp của đơn nguyên thứ hai dành riêng cho đơn vị thứ ba. Khi bạn bắt đầu xếp chồng, các loại cấu trúc này trở nên rất cần thiết. Tôi không bao giờ khởi tạo một FG, tất nhiên; nó chỉ là một bản hack để cho phép tôi thể hiện những gì tôi muốn trong hệ thống loại.


3
Điều thú vị cần lưu ý là Haskell không hỗ trợ trực tiếp lambdas cấp độ , mặc dù một số tin tặc newtype (ví dụ thư viện TypeCompose) có cách để khắc phục điều đó.
Dan Burton

1
Tôi tò mò muốn xem bạn định nghĩa bindphương thức cho EitherMonadlớp của bạn . :-) Ngoài ra, nếu tôi có thể kênh Adriaan trong một giây ở đây, bạn sẽ không sử dụng các loại loại cao hơn trong ví dụ đó. Bạn đang ở FG, nhưng không phải trong EitherMonad. Thay vào đó, bạn đang sử dụng các hàm tạo kiểu , có loại * => *. Loại này là thứ tự 1, không "cao hơn".
Daniel Spiewak

2
Tôi nghĩ rằng loại đó *là thứ tự-1, nhưng trong mọi trường hợp, Monad có loại (* => *) => *. Ngoài ra, bạn sẽ lưu ý rằng tôi đã chỉ định "dự đoán đúng Either[A, B]" - việc triển khai là không đáng kể (nhưng là một bài tập tốt nếu bạn chưa thực hiện trước đó!)
Kris Nuttycombe

Tôi cho rằng quan điểm của Daniel không gọi *=>*cao hơn là hợp lý bởi sự tương tự rằng chúng ta không gọi một hàm thông thường (ánh xạ các hàm không thành hàm không, nói cách khác, giá trị đơn giản thành giá trị đơn giản) hàm bậc cao hơn.
jhegedus

1
Cuốn sách TAPL của Pierce, trang 438:Type expressions with kinds like (*⇒*)⇒* are called higher-order typeoperators.
jhegedus

52

Những lợi ích hoàn toàn giống như những lợi ích được trao bởi các chức năng ẩn danh.

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

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

Một ví dụ sử dụng, với Scalaz 7. Chúng tôi muốn sử dụng một hàm Functorcó thể ánh xạ một hàm qua phần tử thứ hai trong 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 cung cấp một số chuyển đổi ngầm định có thể suy ra đối số loại Functor, vì vậy chúng ta thường tránh viết hoàn toàn các chuyển đổi này. Dòng trước có thể được viết lại thành:

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

Nếu bạn sử dụng IntelliJ, bạn có thể bật Cài đặt, Kiểu mã, Scala, Gấp, Nhập Lambdas. Điều này sau đó ẩn các phần thô lỗ của cú pháp và trình bày rõ ràng hơn:

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

Một phiên bản tương lai của Scala có thể hỗ trợ trực tiếp cú pháp như vậy.


Đoạn trích cuối cùng trông rất đẹp. IntelliJ scala plugin chắc chắn là tuyệt vời!
AndreasScheinert

1
Cảm ơn! Lambda có thể bị thiếu trong ví dụ cuối cùng. Ngoài ra, tại sao các functor tuple chọn chuyển đổi giá trị cuối cùng? Đây có phải là quy ước / một mặc định thực tế?
ron

1
Tôi đang chạy nightlies cho Nika và tôi không có tùy chọn IDEA được mô tả. Điều thú vị đủ, có một kiểm tra cho "Ứng dụng Loại Lambda có thể được đơn giản hóa."
Randall Schulz

6
Nó được chuyển đến Cài đặt -> Trình chỉnh sửa -> Gấp mã.
retronym

@retronym, tôi đã gặp lỗi khi thử (1, 2).map(a => a + 1)trong REPL: `<console>: 11: error: value map không phải là thành viên của (Int, Int) (1, 2) .map (a => a + 1) ^`
Kevin Meredith

41

Để đặt mọi thứ trong bối cảnh: Câu trả lời này ban đầu được đăng trong một chủ đề khác. Bạn đang nhìn thấy nó ở đây vì hai chủ đề đã được hợp nhất. Các câu hỏi trong chủ đề nói như sau:

Làm cách nào để giải quyết định nghĩa loại này: Pure [({type? [A] = (R, a)}) #?]?

Những lý do của việc sử dụng xây dựng như vậy là gì?

Snipped đến từ thư viện 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)
  }

//...
}

Câu trả lời:

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

Một dấu gạch dưới trong các hộp sau khi Pngụ ý rằng nó là một hàm tạo kiểu lấy một kiểu và trả về một kiểu khác. Ví dụ về các loại nhà xây dựng với loại này: List, Option.

Cho Listmột Int, một loại bê tông, và nó cung cấp cho bạn List[Int], một loại bê tông khác. Cho Listmột Stringvà nó cung cấp cho bạn List[String]. Vân vân.

Vì vậy, List, Optioncó thể được coi là chức năng mức loại arity 1. Chính thức chúng ta nói, họ có một loại * -> *. Dấu hoa thị biểu thị một loại.

Bây giờ Tuple2[_, _]là một hàm tạo kiểu với loại (*, *) -> *tức là bạn cần cung cấp cho nó hai loại để có được một kiểu mới.

Kể từ khi chữ ký của họ không phù hợp, bạn không thể thay thế Tuple2cho P. Những gì bạn cần làm là áp dụng một phần Tuple2 vào một trong các đối số của nó, nó sẽ cung cấp cho chúng ta một hàm tạo kiểu với loại * -> *và chúng ta có thể thay thế nó cho P.

Thật không may, Scala không có cú pháp đặc biệt cho ứng dụng một phần của các hàm tạo kiểu, và vì vậy chúng ta phải dùng đến sự quái dị được gọi là kiểu lambdas. (Những gì bạn có trong ví dụ của mình.) Chúng được gọi là bởi vì chúng tương tự như các biểu thức lambda tồn tại ở mức giá trị.

Ví dụ sau có thể giúp:

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

Biên tập:

Nhiều mức giá trị và tương đương mức độ loại.

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

Trong trường hợp bạn đã trình bày, tham số loại Rlà cục bộ để hoạt động Tuple2Purevà do đó bạn không thể xác định đơn giản type PartialTuple2[A] = Tuple2[R, A], vì đơn giản là không có nơi nào bạn có thể đặt từ đồng nghĩa đó.

Để giải quyết trường hợp như vậy, tôi sử dụng thủ thuật sau đây sử dụng các thành viên loại. (Hy vọng ví dụ này là tự giải thích.)

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]gây ra bất cứ điều gì mà chúng ta đưa vào Atrong X[A]các implicitly[X[A] =:= Foo[String,Int]]phải luôn luôn đúng tôi đoán.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.