Hai cách nấu cà ri ở Scala; trường hợp sử dụng cho từng loại là gì?


83

Tôi đang thảo luận xung quanh Danh sách Nhiều Tham số trong Hướng dẫn Kiểu Scala mà tôi duy trì. Tôi nhận ra rằng có hai cách nấu cà ri và tôi tự hỏi các trường hợp sử dụng là gì:

def add(a:Int)(b:Int) = {a + b}
// Works
add(5)(6)
// Doesn't compile
val f = add(5)
// Works
val f = add(5)_
f(10) // yields 15

def add2(a:Int) = { b:Int => a + b }
// Works
add2(5)(6)
// Also works
val f = add2(5)
f(10) // Yields 15
// Doesn't compile
val f = add2(5)_

Hướng dẫn kiểu không chính xác ngụ ý rằng chúng giống nhau, khi chúng rõ ràng là không. Hướng dẫn đang cố gắng làm rõ về các chức năng cà ri đã tạo, và mặc dù dạng thứ hai không phải là cà ri "theo sách", nó vẫn rất giống với dạng đầu tiên (mặc dù được cho là dễ sử dụng hơn vì bạn không cần cái _)

Từ những người sử dụng các biểu mẫu này, đâu là sự đồng thuận về thời điểm sử dụng biểu mẫu này so với biểu mẫu kia?

Câu trả lời:


137

Phương pháp danh sách nhiều tham số

Đối với kiểu suy luận

Các phương thức có nhiều phần tham số có thể được sử dụng để hỗ trợ suy luận kiểu cục bộ, bằng cách sử dụng các tham số trong phần đầu tiên để suy ra các đối số kiểu sẽ cung cấp kiểu mong đợi cho một đối số trong phần tiếp theo. foldLefttrong thư viện chuẩn là ví dụ chính tắc về điều này.

def foldLeft[B](z: B)(op: (B, A) => B): B

List("").foldLeft(0)(_ + _.length)

Nếu điều này được viết như sau:

def foldLeft[B](z: B, op: (B, A) => B): B

Người ta sẽ phải cung cấp các loại rõ ràng hơn:

List("").foldLeft(0, (b: Int, a: String) => a + b.length)
List("").foldLeft[Int](0, _ + _.length)

Đối với API thông thạo

Một cách sử dụng khác cho các phương thức phần nhiều tham số là tạo một API trông giống như một cấu trúc ngôn ngữ. Người gọi có thể sử dụng dấu ngoặc nhọn thay vì dấu ngoặc đơn.

def loop[A](n: Int)(body: => A): Unit = (0 until n) foreach (n => body)

loop(2) {
   println("hello!")
}

Ứng dụng của N danh sách đối số vào phương thức có M phần tham số, trong đó N <M, có thể được chuyển đổi thành hàm một cách rõ ràng với a _hoặc ngầm định, với kiểu mong đợi là FunctionN[..]. Đây là một tính năng an toàn, hãy xem ghi chú thay đổi cho Scala 2.0, trong Tài liệu tham khảo Scala, để biết thông tin cơ bản.

Chức năng Curry

Các hàm có sẵn (hoặc đơn giản là các hàm trả về các hàm) dễ dàng được áp dụng hơn cho N danh sách đối số.

val f = (a: Int) => (b: Int) => (c: Int) => a + b + c
val g = f(1)(2)

Sự tiện lợi nhỏ này đôi khi rất đáng giá. Lưu ý rằng các hàm không thể là kiểu tham số, vì vậy trong một số trường hợp, một phương thức là bắt buộc.

Ví dụ thứ hai của bạn là kết hợp: phương thức phần một tham số trả về một hàm.

Tính toán nhiều giai đoạn

Các chức năng khác của cà ri hữu ích ở đâu? Đây là một mẫu luôn xuất hiện:

def v(t: Double, k: Double): Double = {
   // expensive computation based only on t
   val ft = f(t)

   g(ft, k)
}

v(1, 1); v(1, 2);

Làm thế nào chúng ta có thể chia sẻ kết quả f(t)? Một giải pháp phổ biến là cung cấp một phiên bản vector hóa của v:

def v(t: Double, ks: Seq[Double]: Seq[Double] = {
   val ft = f(t)
   ks map {k => g(ft, k)}
}

Xấu xí! Chúng tôi đã vướng mắc các mối quan tâm không liên quan - tính toán g(f(t), k)và lập bản đồ trên một chuỗi ks.

val v = { (t: Double) =>
   val ft = f(t)
   (k: Double) => g(ft, k)       
}
val t = 1
val ks = Seq(1, 2)
val vs = ks map (v(t))

Chúng ta cũng có thể sử dụng một phương thức trả về một hàm. Trong trường hợp này, nó dễ đọc hơn một chút:

def v(t:Double): Double => Double = {
   val ft = f(t)
   (k: Double) => g(ft, k)       
}

Nhưng nếu chúng ta cố gắng làm điều tương tự với một phương thức có nhiều phần tham số, chúng ta sẽ gặp khó khăn:

def v(t: Double)(k: Double): Double = {
                ^
                `-- Can't insert computation here!
}

3
Câu trả lời tuyệt vời; ước gì tôi có nhiều ủng hộ hơn chỉ một. Tôi sẽ tìm hiểu và áp dụng cho hướng dẫn kiểu; nếu tôi thành công, đây là câu trả lời được lựa chọn ...
davetron5000

1
Bạn có thể muốn sửa ví dụ về vòng lặp của mình thành:def loop[A](n: Int)(body: => A): Unit = (0 until n) foreach (n => body)
Omer van Kloeten

Điều này không biên dịch:val f: (a: Int) => (b: Int) => (c: Int) = a + b + c
nilskp

"Ứng dụng của N danh sách đối số cho phương thức có M phần tham số, trong đó N <M, có thể được chuyển đổi thành hàm một cách rõ ràng với dấu _ hoặc ngầm định, với kiểu mong đợi là FunctionN [..]." <br/> Nó không phải là FunctionX [..], trong đó X = MN?
stackoverflower

"Điều này không biên dịch: val f: (a: Int) => (b: Int) => (c: Int) = a + b + c" Tôi không nghĩ "f: (a: Int) = > (b: Int) => (c: Int) "là cú pháp đúng. Có lẽ từ viết tắt có nghĩa là "f: Int => Int => Int => Int". Bởi vì => là liên kết đúng, đây thực sự là "f: Int => (Int => (Int => Int))". Vì vậy, f (1) (2) là loại Int => Int (có nghĩa là, các bit nhất bên trong f của loại)
stackoverflower

16

Bạn chỉ có thể giải thích các hàm chứ không phải các phương pháp. addlà một phương thức, vì vậy bạn cần _bắt buộc chuyển đổi nó thành một hàm. add2trả về một hàm, vì vậy hàm _không chỉ không cần thiết mà còn vô nghĩa ở đây.

Xét như thế nào phương pháp và chức năng khác nhau (ví dụ như từ quan điểm của JVM), Scala làm một công việc tốt đẹp làm mờ ranh giới giữa họ và làm "The Right Thing" trong hầu hết các trường hợp, nhưng có một sự khác biệt, và đôi khi bạn chỉ cần để biết về nó.


Điều này có lý, vì vậy bạn gọi biểu mẫu def add (a: Int) (b: Int) sau đó là gì? Thuật ngữ / cụm từ mô tả sự khác biệt giữa bổ sung def và def đó là gì (a: Int, b: Int)?
davetron5000

@ davetron5000 cái đầu tiên là một phương thức có nhiều danh sách tham số, cái thứ hai là một phương thức với một danh sách tham số.
Jesper

5

Tôi nghĩ sẽ giúp nắm bắt được sự khác biệt nếu tôi thêm điều đó với def add(a: Int)(b: Int): Intbạn khá nhiều chỉ cần xác định một phương thức với hai tham số, chỉ hai tham số đó được nhóm thành hai danh sách tham số (xem hậu quả của điều đó trong các nhận xét khác). Trên thực tế, phương pháp đó cũng giống int add(int a, int a)như Java (không phải Scala!). Khi bạn viết add(5)_, đó chỉ là một nghĩa đen của hàm, một dạng ngắn hơn của { b: Int => add(1)(b) }. Mặt khác, với việc add2(a: Int) = { b: Int => a + b }bạn xác định một phương thức chỉ có một tham số và đối với Java thì sẽ như vậy scala.Function add2(int a). Khi bạn viết add2(1)bằng Scala, nó chỉ là một cuộc gọi phương thức đơn giản (trái ngược với một nghĩa đen của hàm).

Cũng lưu ý rằng addcó (có thể) ít chi phí hơn add2có nếu bạn cung cấp ngay lập tức tất cả các tham số. Giống như add(5)(6)chỉ dịch sang add(5, 6)cấp độ JVM, không có Functionđối tượng nào được tạo. Mặt khác, add2(5)(6)đầu tiên sẽ tạo một Functionđối tượng bao quanh 5, và sau đó gọi apply(6)đối tượng đó.

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.