Scala currying so với các chức năng áp dụng một phần


82

Tôi nhận ra rằng có một số câu hỏi trên đây về những gì currying và chức năng áp dụng một phần là, nhưng tôi đang hỏi về cách họ là khác nhau. Ví dụ đơn giản, đây là một hàm curry để tìm các số chẵn:

def filter(xs: List[Int], p: Int => Boolean): List[Int] =
   if (xs.isEmpty) xs
   else if (p(xs.head)) xs.head :: filter(xs.tail, p)
   else filter(xs.tail, p)

def modN(n: Int)(x: Int) = ((x % n) == 0)

Vì vậy, bạn có thể viết như sau để sử dụng:

val nums = List(1,2,3,4,5,6,7,8)
println(filter(nums, modN(2))

mà lợi nhuận: List(2,4,6,8). Nhưng tôi thấy rằng tôi có thể làm điều tương tự theo cách này:

def modN(n: Int, x: Int) = ((x % n) == 0)

val p = modN(2, _: Int)
println(filter(nums, p))

mà cũng trả về: List(2,4,6,8).

Vì vậy, câu hỏi của tôi là, sự khác biệt chính giữa hai cái này là gì và khi nào thì bạn sử dụng cái này thay cho cái kia? Đây có phải là một ví dụ quá đơn giản để chỉ ra lý do tại sao cái này lại được sử dụng thay cho cái kia không?


Khi được áp dụng một phần, chi phí của việc thể hiện phiên bản cà ri và không có cà ri trong bộ nhớ thể khác nhau, do đó, hiệu suất thời gian chạy cũng có thể bị ảnh hưởng. (Có nghĩa là, nếu trình tối ưu hóa không đủ thông minh để chọn đại diện tối ưu trong cả hai trường hợp.) Tuy nhiên, tôi không đủ quen thuộc với Scala để nói sự khác biệt chính xác là gì.

1
Có một cái nhìn tại một này: stackoverflow.com/questions/8063325/...
Utaal

Tôi tìm thấy lời giải thích này rất rất hữu ích, ông giải thích về chức năng một phần, chức năng áp dụng một phần và currying, tất cả trong một bài: stackoverflow.com/a/8650639/1287554
PLASTY Grove

Liên kết tuyệt vời, @PlastyGrove. Cảm ơn bạn!
Eric

Và cảm ơn bạn @Utaal về liên kết của bạn. Bất kỳ câu trả lời nào từ bản thân Martin Odersky đều rất đáng giá. Tôi nghĩ rằng những khái niệm này đang bắt đầu nhấp vào bây giờ.
Eric

Câu trả lời:


88

Sự khác biệt về ngữ nghĩa đã được giải thích khá rõ trong câu trả lời được liên kết bởi Plasty Grove .

Về mặt chức năng, dường như không có nhiều sự khác biệt. Hãy xem một số ví dụ để xác minh điều đó. Đầu tiên, một chức năng bình thường:

scala> def modN(n: Int, x: Int): Boolean = ((x % n) == 0)
scala> modN(5, _ : Int)
res0: Int => Boolean = <function1>

Vì vậy, chúng tôi nhận được áp dụng một phần <function1>nhận một Int, bởi vì chúng tôi đã cung cấp cho nó số nguyên đầu tiên. Càng xa càng tốt. Bây giờ đến món cà ri:

scala> def modNCurried(n: Int)(x: Int): Boolean = ((x % n) == 0)

Với ký hiệu này, bạn sẽ ngây thơ mong đợi những điều sau sẽ hoạt động:

scala> modNCurried(5)
<console>:9: error: missing arguments for method modN;
follow this method with `_' if you want to treat it as a partially applied function
          modNCurried(5)

Vì vậy, ký hiệu danh sách nhiều tham số dường như không thực sự tạo ra một hàm curry ngay lập tức (giả sử là để tránh chi phí không cần thiết) mà đợi bạn tuyên bố rõ ràng rằng bạn muốn nó được thực hiện (ký hiệu này cũng có một số lợi thế khác ):

scala> modNCurried(5) _
res24: Int => Boolean = <function1>

Đó chính xác là thứ chúng ta đã có trước đây, vì vậy không có sự khác biệt ở đây, ngoại trừ ký hiệu. Một vi dụ khac:

scala> modN _
res35: (Int, Int) => Boolean = <function2>

scala> modNCurried _
res36: Int => (Int => Boolean) = <function1>

Điều này cho thấy cách áp dụng một phần hàm "bình thường" dẫn đến một hàm nhận tất cả các tham số, trong khi việc áp dụng một phần một hàm với nhiều danh sách tham số sẽ tạo ra một chuỗi hàm, mỗi hàm trên một danh sách tham số , tất cả đều trả về một hàm mới:

scala> def foo(a:Int, b:Int)(x:Int)(y:Int): Int = a * b + x - y
scala> foo _
res42: (Int, Int) => Int => (Int => Int) = <function2>

scala> res42(5)
<console>:10: error: not enough arguments for method apply: (v1: Int, v2: Int)Int => (Int => Int) in trait Function2.
Unspecified value parameter v2.

Như bạn có thể thấy, vì danh sách tham số đầu tiên foocó hai tham số, nên hàm đầu tiên trong chuỗi cà ri có hai tham số.


Tóm lại, các hàm được áp dụng một phần không thực sự khác biệt về hình thức các hàm được xử lý về mặt chức năng. Điều này dễ dàng được xác minh vì bạn có thể chuyển đổi bất kỳ chức năng nào thành chức năng được nấu chín:

scala> (modN _).curried
res45: Int => (Int => Boolean) = <function1

scala> modNCurried _
res46: Int => (Int => Boolean) = <function1>

Đoạn tái bút

Lưu ý: Lý do mà ví dụ của bạn println(filter(nums, modN(2))hoạt động mà không có dấu gạch dưới sau modN(2)dường như là trình biên dịch Scala chỉ đơn giản giả định rằng dấu gạch dưới như một sự tiện lợi cho người lập trình.


Bổ sung: Như @asflierl đã chỉ ra một cách chính xác, Scala dường như không thể suy ra loại khi áp dụng một phần các hàm "bình thường":

scala> modN(5, _)
<console>:9: error: missing parameter type for expanded function ((x$1) => modN(5, x$1))

Trong khi thông tin đó có sẵn cho các hàm được viết bằng cách sử dụng ký hiệu danh sách nhiều tham số:

scala> modNCurried(5) _
res3: Int => Boolean = <function1>

Câu trả lời này cho thấy điều này có thể rất hữu ích như thế nào.


một sự khác biệt quan trọng là suy luận kiểu công trình khác nhau: gist.github.com/4529020

Cảm ơn, tôi đã thêm một lưu ý về nhận xét của bạn :)
fresskoma

19

Currying liên quan đến các bộ giá trị: biến một hàm nhận một đối số bộ thành một hàm nhận n đối số riêng biệt và ngược lại . Ghi nhớ điều này là chìa khóa để phân biệt cà ri và ứng dụng một phần, ngay cả trong các ngôn ngữ không hỗ trợ cà ri.

curry :: ((a, b) -> c) -> a -> b -> c 
   -- curry converts a function that takes all args in a tuple
   -- into one that takes separate arguments

uncurry :: (a -> b -> c) -> (a, b) -> c
   -- uncurry converts a function of separate args into a function on pairs.

Ứng dụng một phần là khả năng áp dụng một hàm cho một số đối số, mang lại một hàm mới cho các đối số còn lại .

Sẽ rất dễ nhớ nếu bạn chỉ nghĩ rằng cà ri là một sự biến đổi để làm với tuples.

Trong các ngôn ngữ được xử lý theo mặc định (chẳng hạn như Haskell), sự khác biệt rất rõ ràng - bạn phải thực sự làm điều gì đó để chuyển các đối số trong một bộ tuple. Nhưng hầu hết các ngôn ngữ khác, bao gồm cả Scala, đều không được kiểm tra theo mặc định - tất cả các args đều được chuyển dưới dạng bộ giá trị, vì vậy curry / unsurry ít hữu dụng hơn và ít rõ ràng hơn. Và mọi người thậm chí còn nghĩ rằng ứng dụng một phần và nấu cà ri là cùng một thứ - chỉ vì chúng không thể biểu diễn các chức năng của món cà ri một cách dễ dàng!


Tôi hoàn toàn đồng ý. Theo thuật ngữ Scala, "currying" trong nghĩa gốc của từ này là "quá trình chuyển đổi" một hàm có một danh sách tham số thành một hàm có nhiều danh sách tham số. Trong Scala, phép biến đổi này có thể được thực thi bằng cách sử dụng ".curried". Thật không may, Scala dường như đã quá tải nghĩa của từ này một chút vì ban đầu nó thà được gọi là ".curry" thay vì ".curried".
Fatih Coşkun

2

Chức năng đa biến:

def modN(n: Int, x: Int) = ((x % n) == 0)

Currying (hoặc chức năng nấu cà ri):

def modNCurried(n: Int)(x: Int) = ((x % n) == 0)

Vì vậy, nó không được áp dụng một phần chức năng có thể so sánh với cà ri. Đó là chức năng đa biến. Điều có thể so sánh với hàm được áp dụng một phần là kết quả gọi của một hàm curried, là một hàm có cùng danh sách tham số mà hàm được áp dụng một phần có.


0

Chỉ để làm rõ về điểm cuối cùng

Bổ sung: Như @asflierl đã chỉ ra chính xác, Scala dường như không thể suy ra loại khi áp dụng một phần các hàm "bình thường":

Scala có thể suy ra các loại nếu tất cả các tham số là ký tự đại diện nhưng không phải khi một số trong số chúng được chỉ định và một số trong số chúng thì không.

scala> modN(_,_)
res38: (Int, Int) => Boolean = <function2>

scala> modN(1,_)
<console>:13: error: missing parameter type for expanded function ((x$1) => modN(1, x$1))
       modN(1,_)
              ^

0

Lời giải thích tốt nhất mà tôi có thể tìm thấy cho đến nay: https://dzone.com/articles/difference-between-currying-amp-partially-applied

Currying: phân rã các hàm có nhiều đối số thành một chuỗi các hàm một đối số. Lưu ý rằng Scala cho phép truyền một hàm làm đối số cho một hàm khác.

Ứng dụng một phần của hàm: truyền cho một hàm ít đối số hơn nó có trong phần khai báo. Scala không ném ra một ngoại lệ khi bạn cung cấp ít đối số hơn cho hàm, nó chỉ đơn giản áp dụng chúng và trả về một hàm mới với phần còn lại của các đối số cần được truyề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.