Lợi thế của cà ri là gì?


154

Tôi mới học về cà ri, và trong khi tôi nghĩ rằng tôi hiểu khái niệm này, tôi không thấy bất kỳ lợi thế lớn nào khi sử dụng nó.

Như một ví dụ tầm thường, tôi sử dụng một hàm có thêm hai giá trị (được viết bằng ML). Phiên bản không có cà ri sẽ là

fun add(x, y) = x + y

và sẽ được gọi là

add(3, 5)

trong khi phiên bản cà ri là

fun add x y = x + y 
(* short for val add = fn x => fn y=> x + y *)

và sẽ được gọi là

add 3 5

Đối với tôi, dường như chỉ là đường cú pháp loại bỏ một bộ dấu ngoặc đơn để xác định và gọi hàm. Tôi đã thấy cà ri được liệt kê là một trong những tính năng quan trọng của ngôn ngữ chức năng và hiện tại tôi hơi bị choáng ngợp bởi nó. Khái niệm tạo một chuỗi các hàm tiêu thụ từng tham số đơn lẻ, thay vì một hàm mất một bộ dữ liệu có vẻ khá phức tạp để sử dụng cho một thay đổi cú pháp đơn giản.

Là cú pháp đơn giản hơn một chút là động lực duy nhất cho cà ri, hay tôi thiếu một số lợi thế khác không rõ ràng trong ví dụ rất đơn giản của tôi? Là cà ri chỉ là cú pháp đường?


54
Curry một mình về cơ bản là vô dụng, nhưng mặc định tất cả các chức năng được làm cho nhiều tính năng khác dễ sử dụng hơn nhiều. Thật khó để đánh giá cao điều này cho đến khi bạn thực sự sử dụng một ngôn ngữ chức năng trong một thời gian.
CA McCann

4
Một cái gì đó đã được đề cập khi đi ngang qua delnan trong một nhận xét về câu trả lời của JoelEtherton, nhưng tôi nghĩ tôi sẽ đề cập rõ ràng, đó là (ít nhất là trong Haskell) bạn có thể áp dụng một phần với không chỉ các hàm mà cả các hàm tạo - điều này có thể khá tiện dụng; đây có thể là một cái gì đó để suy nghĩ
paul

Tất cả đã đưa ra ví dụ về Haskell. Người ta có thể tự hỏi cà ri chỉ hữu ích trong Haskell.
Manoj R

@ManojR Tất cả đã không đưa ra ví dụ trong Haskell.
phwd

1
Câu hỏi tạo ra một cuộc thảo luận khá thú vị trên Reddit .
yannis

Câu trả lời:


126

Với các chức năng được xử lý, bạn có thể sử dụng lại các chức năng trừu tượng dễ dàng hơn kể từ khi bạn chuyên môn hóa. Hãy nói rằng bạn có một chức năng thêm

add x y = x + y

và bạn muốn thêm 2 vào mỗi thành viên trong danh sách. Trong Haskell bạn sẽ làm điều này:

map (add 2) [1, 2, 3] -- gives [3, 4, 5]
-- actually one could just do: map (2+) [1, 2, 3], but that may be Haskell specific

Ở đây cú pháp nhẹ hơn nếu bạn phải tạo một hàm add2

add2 y = add 2 y
map add2 [1, 2, 3]

hoặc nếu bạn phải tạo một hàm lambda ẩn danh:

map (\y -> 2 + y) [1, 2, 3]

Nó cũng cho phép bạn trừu tượng ra khỏi các triển khai khác nhau. Giả sử bạn có hai chức năng tra cứu. Một từ danh sách các cặp khóa / giá trị và một khóa cho một giá trị và một từ bản đồ từ khóa đến giá trị và khóa thành giá trị, như sau:

lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value

Sau đó, bạn có thể tạo một hàm chấp nhận chức năng tra cứu từ Khóa thành Giá trị. Bạn có thể vượt qua nó bất kỳ chức năng tra cứu nào ở trên, được áp dụng một phần với danh sách hoặc bản đồ, tương ứng:

myFunc :: (Key -> Value) -> .....

Tóm lại: currying là tốt, bởi vì nó cho phép bạn chuyên dụng / một phần các hàm sử dụng cú pháp nhẹ và sau đó chuyển các hàm được áp dụng một phần này xung quanh cho hàm bậc cao hơn như maphoặc filter. Các hàm bậc cao hơn (lấy các hàm làm tham số hoặc mang lại kết quả) là bánh mì và bơ của lập trình hàm, và các hàm được áp dụng một phần cho phép các hàm bậc cao hơn được sử dụng hiệu quả và chính xác hơn nhiều.


31
Điều đáng chú ý là, vì điều này, thứ tự đối số được sử dụng cho các hàm trong Haskell thường dựa trên khả năng ứng dụng một phần, từ đó làm cho các lợi ích được mô tả ở trên áp dụng (ha, ha) trong nhiều tình huống hơn. Curry theo mặc định do đó cuối cùng thậm chí còn có lợi hơn là rõ ràng từ các ví dụ cụ thể như những ví dụ ở đây.
CA McCann

wat. "Một từ danh sách các cặp khóa / giá trị và một khóa cho một giá trị và một từ bản đồ từ khóa đến giá trị và khóa thành giá trị"
Mateen Ulhaq

@MateenUlhaq Đây là phần tiếp theo của câu trước, trong đó tôi cho rằng chúng tôi muốn nhận được một giá trị dựa trên khóa và chúng tôi có hai cách để làm điều đó. Câu liệt kê hai cách đó. Theo cách thứ nhất, bạn được cung cấp một danh sách các cặp khóa / giá trị và khóa mà chúng tôi muốn tìm giá trị và theo cách khác, chúng tôi được cung cấp một bản đồ phù hợp và lại là một khóa. Nó có thể giúp để xem mã ngay sau câu.
Boris

53

Câu trả lời thực tế là cà ri giúp tạo ra các chức năng ẩn danh dễ dàng hơn nhiều. Ngay cả với một cú pháp lambda tối thiểu, đó là một chiến thắng; so sánh:

map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]

Nếu bạn có một cú pháp lambda xấu xí, nó thậm chí còn tệ hơn. (Tôi đang nhìn bạn, JavaScript, Scheme và Python.)

Điều này ngày càng trở nên hữu ích khi bạn sử dụng ngày càng nhiều hàm bậc cao hơn. Mặc dù tôi sử dụng các hàm bậc cao hơn trong Haskell so với các ngôn ngữ khác, tôi thực sự thấy tôi sử dụng cú pháp lambda ít hơn bởi vì khoảng hai phần ba thời gian, lambda sẽ chỉ là một hàm được áp dụng một phần. (Và phần lớn thời gian khác tôi trích xuất nó thành một hàm được đặt tên.)

Về cơ bản hơn, không phải lúc nào cũng rõ ràng phiên bản của chức năng là "chính tắc". Ví dụ, lấy map. Loại mapcó thể được viết theo hai cách:

map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])

Cái nào là "đúng"? Thật sự rất khó để nói. Trong thực tế, hầu hết các ngôn ngữ sử dụng cái đầu tiên - bản đồ có chức năng và danh sách và trả về danh sách. Tuy nhiên, về cơ bản, những gì bản đồ thực sự làm là ánh xạ các hàm bình thường để liệt kê các hàm - nó lấy một hàm và trả về một hàm. Nếu bản đồ bị quấy rầy, bạn không cần phải trả lời câu hỏi này: nó thực hiện cả hai , theo một cách rất thanh lịch.

Điều này trở nên đặc biệt quan trọng khi bạn khái quát hóa mapcác loại khác ngoài danh sách.

Ngoài ra, cà ri thực sự không quá phức tạp. Đó thực sự là một chút đơn giản hóa đối với mô hình mà hầu hết các ngôn ngữ sử dụng: bạn không cần bất kỳ khái niệm nào về chức năng của nhiều đối số được đưa vào ngôn ngữ của bạn. Điều này cũng phản ánh tính toán lambda cơ bản chặt chẽ hơn.

Tất nhiên, các ngôn ngữ kiểu ML không có khái niệm về nhiều đối số ở dạng bị cắt hoặc ở dạng chưa được xử lý. Các f(a, b, c)cú pháp thực sự tương ứng với đi qua trong tuple (a, b, c)vào f, vì vậy fvẫn chỉ mất trên lập luận. Đây thực sự là một sự phân biệt rất hữu ích mà tôi mong muốn các ngôn ngữ khác sẽ có vì nó rất tự nhiên để viết một cái gì đó như:

map f [(1,2,3), (4,5,6), (7, 8, 9)]

Bạn không thể dễ dàng làm điều này với các ngôn ngữ có ý tưởng về nhiều đối số được đưa vào ngay!


1
"Các ngôn ngữ theo kiểu ML không có khái niệm về nhiều đối số ở dạng bị cắt hoặc ở dạng chưa được xử lý": về mặt này, là kiểu Haskell ML?
Giorgio

1
@Giorgio: Vâng.
Tikhon Jelvis

1
Hấp dẫn. Tôi biết một số Haskell và tôi đang học SML ngay bây giờ, vì vậy thật thú vị khi thấy sự khác biệt và tương đồng giữa hai ngôn ngữ.
Giorgio

Câu trả lời tuyệt vời và nếu bạn vẫn chưa bị thuyết phục, hãy nghĩ về các đường ống Unix tương tự như các luồng lambda
Sridhar Sarnobat

Câu trả lời "thực tế" không liên quan nhiều vì tính dài dòng thường được tránh bằng cách áp dụng một phần , không phải là cà ri. Và tôi tranh luận ở đây cú pháp trừu tượng lambda (mặc dù khai báo kiểu) xấu hơn so với (ít nhất) trong Scheme vì nó cần nhiều quy tắc cú pháp đặc biệt tích hợp sẵn để phân tích chính xác, làm mờ thông số ngôn ngữ mà không đạt được về tính chất ngữ nghĩa.
FrankHB

24

Currying có thể hữu ích nếu bạn có một hàm mà bạn đang chuyển qua như một đối tượng hạng nhất và bạn không nhận được tất cả các tham số cần thiết để đánh giá nó ở một nơi trong mã. Bạn có thể chỉ cần áp dụng một hoặc nhiều tham số khi bạn nhận được chúng và chuyển kết quả cho một đoạn mã khác có nhiều tham số hơn và hoàn thành việc đánh giá nó ở đó.

Mã để thực hiện điều này sẽ đơn giản hơn nếu bạn cần phải có được tất cả các tham số cùng nhau trước tiên.

Ngoài ra, có khả năng tái sử dụng nhiều mã hơn, vì các hàm lấy một tham số duy nhất (một hàm bị cong khác) không phải khớp như cụ thể với tất cả các tham số.


14

Động lực chính (ít nhất là ban đầu) cho cà ri không phải là thực tế mà là lý thuyết. Cụ thể, currying cho phép bạn có được các hàm đa đối số một cách hiệu quả mà không thực sự xác định ngữ nghĩa cho chúng hoặc xác định ngữ nghĩa cho các sản phẩm. Điều này dẫn đến một ngôn ngữ đơn giản hơn với nhiều biểu cảm như một ngôn ngữ khác, phức tạp hơn, và do đó là mong muốn.


2
Trong khi động lực ở đây là lý thuyết, tôi nghĩ rằng sự đơn giản hầu như luôn luôn là một lợi thế thực tế. Không phải lo lắng về các hàm đa đối số giúp cuộc sống của tôi dễ dàng hơn khi tôi lập trình, giống như khi tôi làm việc với ngữ nghĩa.
Tikhon Jelvis

2
@TikhonJelvis Tuy nhiên, khi bạn lập trình, việc currying mang đến cho bạn những điều khác cần lo lắng, như trình biên dịch không nắm bắt được thực tế là bạn đã truyền quá ít đối số cho một hàm hoặc thậm chí nhận được thông báo lỗi xấu trong trường hợp đó; Khi bạn không sử dụng cà ri, lỗi sẽ rõ ràng hơn nhiều.
Alex R

Tôi chưa bao giờ gặp vấn đề như vậy: ít nhất, GHC, rất tốt trong vấn đề đó. Trình biên dịch luôn nắm bắt được loại vấn đề đó và cũng có thông báo lỗi tốt cho lỗi này.
Tikhon Jelvis

1
Tôi không thể đồng ý rằng các thông báo lỗi đủ điều kiện là tốt. Phục vụ, có, nhưng họ vẫn chưa tốt. Nó cũng chỉ nắm bắt được loại vấn đề đó nếu nó gây ra lỗi loại, tức là nếu sau này bạn cố gắng sử dụng kết quả như một hàm khác (hoặc bạn đã gõ chú thích, nhưng dựa vào đó để biết các lỗi có thể đọc được có vấn đề riêng ); vị trí được báo cáo của lỗi được tách ra khỏi vị trí thực tế của nó.
Alex R

14

(Tôi sẽ đưa ra ví dụ trong Haskell.)

  1. Khi sử dụng các ngôn ngữ chức năng, rất thuận tiện khi bạn có thể áp dụng một phần chức năng. Giống như trong Haskell (== x)là một hàm trả về Truenếu đối số của nó bằng với một thuật ngữ đã cho x:

    mem :: Eq a => a -> [a] -> Bool
    mem x lst = any (== x) lst
    

    không có cà ri, chúng tôi sẽ có một số mã ít đọc hơn:

    mem x lst = any (\y -> y == x) lst
    
  2. Điều này có liên quan đến lập trình Tacit (xem thêm kiểu Pointfree trên wiki Haskell). Phong cách này không tập trung vào các giá trị được biểu thị bằng các biến, mà tập trung vào các hàm và cách thông tin chảy qua một chuỗi các hàm. Chúng ta có thể chuyển đổi ví dụ của mình thành một dạng hoàn toàn không sử dụng biến:

    mem = any . (==)
    

    Ở đây chúng tôi xem ==như là một chức năng từ ađến a -> Boolanynhư là một chức năng từ a -> Boolđến [a] -> Bool. Chỉ cần sáng tác chúng, chúng tôi nhận được kết quả. Tất cả là nhờ cà ri.

  3. Ngược lại, không cà ri, cũng hữu ích trong một số tình huống. Ví dụ: giả sử chúng tôi muốn chia một danh sách thành hai phần - các phần tử nhỏ hơn 10 và phần còn lại, sau đó ghép hai danh sách đó lại. Việc chia danh sách được thực hiện bởi (ở đây chúng tôi cũng sử dụng curried ). Kết quả là loại . Thay vì trích xuất kết quả vào phần thứ nhất và phần thứ hai của nó và kết hợp chúng bằng cách sử dụng , chúng ta có thể thực hiện điều này trực tiếp bằng cách giải quyết nhưpartition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

Thật vậy, (uncurry (++) . partition (< 10)) [4,12,11,1]đánh giá để [4,1,12,11].

Ngoài ra còn có những lợi thế lý thuyết quan trọng:

  1. Currying rất cần thiết cho các ngôn ngữ thiếu các loại dữ liệu và chỉ có các chức năng, chẳng hạn như phép tính lambda . Mặc dù các ngôn ngữ này không hữu ích cho sử dụng thực tế, nhưng chúng rất quan trọng từ quan điểm lý thuyết.
  2. Điều này được kết nối với thuộc tính thiết yếu của các ngôn ngữ chức năng - các chức năng là đối tượng hạng nhất. Như chúng ta đã thấy, việc chuyển đổi từ (a, b) -> cthành a -> (b -> c)có nghĩa là kết quả của hàm sau là loại b -> c. Nói cách khác, kết quả là một chức năng.
  3. (Un) currying được kết nối chặt chẽ với các thể loại đóng cartesian , đó là một cách phân loại để xem tính toán lambda đánh máy.

Đối với bit "mã ít đọc hơn", không nên như mem x lst = any (\y -> y == x) lstvậy? (Với dấu gạch chéo ngược).
stusmith

Vâng, cảm ơn vì đã chỉ ra điều đó, tôi sẽ sửa nó.
Petr Pudlák

9

Currying không chỉ là cú pháp đường!

Xem xét các chữ ký loại của add1(chưa được xử lý) và add2(bị cấm ):

add1 : (int * int) -> int
add2 : int -> (int -> int)

(Trong cả hai trường hợp, dấu ngoặc đơn trong chữ ký loại là tùy chọn, nhưng tôi đã bao gồm chúng cho rõ ràng.)

add1là một chức năng mà phải mất một 2-tuple của intintvà trả về một int. add2là một hàm lấy một intvà trả về một hàm khác lần lượt lấy intvà trả về một int.

Sự khác biệt cơ bản giữa hai trở nên rõ ràng hơn khi chúng ta chỉ định rõ ràng ứng dụng chức năng. Hãy xác định một hàm (không được xử lý) áp ​​dụng đối số thứ nhất cho đối số thứ hai của nó:

apply(f, b) = f b

Bây giờ chúng ta có thể thấy sự khác biệt giữa add1add2rõ ràng hơn. add1được gọi với 2-tuple:

apply(add1, (3, 5))

nhưng add2được gọi với một int và sau đó giá trị trả về của nó được gọi với một giá trị khácint :

apply(apply(add2, 3), 5)

EDIT: Lợi ích thiết yếu của cà ri là bạn có được một phần ứng dụng miễn phí. Hãy nói rằng bạn muốn một hàm loại int -> int(giả sử, mapnó trên một danh sách) đã thêm 5 vào tham số của nó. Bạn có thể viết addFiveToParam x = x+5, hoặc bạn có thể làm tương đương với lambda nội tuyến, nhưng bạn cũng có thể dễ dàng hơn nhiều (đặc biệt là trong trường hợp ít tầm thường hơn cái này) viết add2 5!


3
Tôi hiểu rằng có một sự khác biệt lớn đằng sau hậu trường cho ví dụ của tôi, nhưng kết quả có vẻ là một thay đổi cú pháp đơn giản.
Nhà khoa học điên

5
Currying không phải là một khái niệm rất sâu sắc. Đó là về việc đơn giản hóa mô hình cơ bản (xem Lambda Tính) hoặc trong các ngôn ngữ có bộ dữ liệu dù sao nó thực sự là về sự tiện lợi cú pháp của ứng dụng một phần. Đừng đánh giá thấp tầm quan trọng của sự thuận tiện cú pháp.
Peaker

9

Currying chỉ là cú pháp đường, nhưng bạn hơi hiểu nhầm những gì đường làm, tôi nghĩ. Lấy ví dụ của bạn,

fun add x y = x + y

thực sự là đường tổng hợp cho

fun add x = fn y => x + y

Nghĩa là, (thêm x) trả về một hàm lấy một đối số y và thêm x vào y.

fun addTuple (x, y) = x + y

Đó là một hàm mất một tuple và thêm các phần tử của nó. Hai chức năng này thực sự khá khác nhau; họ có những lý lẽ khác nhau.

Nếu bạn muốn thêm 2 vào tất cả các số trong danh sách:

(* add 2 to all numbers using the uncurried function *)
map (fn x => addTuple (x, 2)) [1,2,3]
(* using the curried function *)
map (add 2) [1,2,3]

Kết quả sẽ là [3,4,5].

Nếu bạn muốn tính tổng từng bộ trong một danh sách, mặt khác, hàm addTuple hoàn toàn phù hợp.

(* Sum each tuple using the uncurried function *)
map addTuple [(10,2), (10,3), (10,4)]    
(* sum each tuple using curried function *)
map (fn (a,b) => add a b) [(10,2), (10,3), (10,4)]

Kết quả sẽ là [12,13,14].

Các hàm curried rất tuyệt vời khi ứng dụng một phần hữu ích - ví dụ: map, Fold, app, filter. Hãy xem xét hàm này, trả về số dương lớn nhất trong danh sách được cung cấp hoặc 0 nếu không có số dương:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 

1
Tôi đã hiểu rằng hàm curried có chữ ký loại khác và nó thực sự là một hàm trả về một hàm khác. Tôi đã bị mất một phần ứng dụng mặc dù.
Nhà khoa học điên

9

Một điều khác tôi chưa thấy đề cập đến là cà ri cho phép (hạn chế) sự trừu tượng đối với arity.

Hãy xem xét các chức năng này là một phần của thư viện của Haskell

(.) :: (b -> c) -> (a -> b) -> a -> c
either :: (a -> c) -> (b -> c) -> Either a b -> c
flip :: (a -> b -> c) -> b -> a -> c
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c

Trong mỗi trường hợp, biến kiểu ccó thể là một kiểu hàm để các hàm này hoạt động trên một số tiền tố của danh sách tham số của đối số của chúng. Nếu không có cà ri, bạn sẽ cần một tính năng ngôn ngữ đặc biệt để trừu tượng hóa tính chất của chức năng hoặc có nhiều phiên bản khác nhau của các chức năng này dành riêng cho các loại hương liệu khác nhau.


6

Hiểu biết hạn chế của tôi là như vậy:

1) Ứng dụng một phần chức năng

Ứng dụng chức năng một phần là quá trình trả về một hàm có số lượng đối số ít hơn. Nếu bạn cung cấp 2 trong số 3 đối số, nó sẽ trả về một hàm có 3-2 = 1 đối số. Nếu bạn cung cấp 1 trong 3 đối số, nó sẽ trả về một hàm có 3-1 = 2 đối số. Nếu bạn muốn, bạn thậm chí có thể áp dụng một phần 3 trong số 3 đối số và nó sẽ trả về một hàm không có đối số.

Vì vậy, đưa ra các chức năng sau:

f(x,y,z) = x + y + z;

Khi liên kết 1 đến x và áp dụng một phần cho chức năng trên, f(x,y,z)bạn sẽ nhận được:

f(1,y,z) = f'(y,z);

Ở đâu: f'(y,z) = 1 + y + z;

Bây giờ nếu bạn đã liên kết y với 2 và z thành 3, và áp dụng một phần f'(y,z)bạn sẽ nhận được:

f'(2,3) = f''();

Trong đó : f''() = 1 + 2 + 3;

Bây giờ tại bất kỳ thời điểm nào, bạn có thể chọn để đánh giá f, f'hoặc f''. Vì vậy, tôi có thể làm:

print(f''()) // and it would return 6;

hoặc là

print(f'(1,1)) // and it would return 3;

2) Cà ri

Mặt khác, Curry là quá trình phân tách một hàm thành một chuỗi lồng nhau của một hàm đối số. Bạn không bao giờ có thể cung cấp nhiều hơn 1 đối số, đó là một hoặc không.

Vì vậy, đưa ra chức năng tương tự:

f(x,y,z) = x + y + z;

Nếu bạn chế biến nó, bạn sẽ nhận được một chuỗi gồm 3 chức năng:

f'(x) -> f''(y) -> f'''(z)

Ở đâu:

f'(x) = x + f''(y);

f''(y) = y + f'''(z);

f'''(z) = z;

Bây giờ nếu bạn gọi f'(x)với x = 1:

f'(1) = 1 + f''(y);

Bạn được trả về một chức năng mới:

g(y) = 1 + f''(y);

Nếu bạn gọi g(y)với y = 2:

g(2) = 1 + 2 + f'''(z);

Bạn được trả về một chức năng mới:

h(z) = 1 + 2 + f'''(z);

Cuối cùng nếu bạn gọi h(z)với z = 3:

h(3) = 1 + 2 + 3;

Bạn được trả lại 6.

3) Đóng cửa

Cuối cùng, Đóng cửa là quá trình thu thập một chức năng và dữ liệu với nhau dưới dạng một đơn vị. Việc đóng hàm có thể mất 0 đến vô số đối số, nhưng cũng nhận thức được dữ liệu không được truyền cho nó.

Một lần nữa, đưa ra chức năng tương tự:

f(x,y,z) = x + y + z;

Thay vào đó, bạn có thể viết một bao đóng:

f(x) = x + f'(y, z);

Ở đâu:

f'(y,z) = x + y + z;

f'đóng cửa vào x. Có nghĩa là f'có thể đọc giá trị của x đó bên trong f.

Vì vậy, nếu bạn đã gọi fvới x = 1:

f(1) = 1 + f'(y, z);

Bạn sẽ nhận được một đóng cửa:

closureOfF(y, z) =
                   var x = 1;
                   f'(y, z);

Bây giờ nếu bạn gọi closureOfFvới y = 2z = 3:

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 3;

Mà sẽ trở lại 6

Phần kết luận

Curry, ứng dụng một phần và đóng cửa đều hơi giống nhau ở chỗ chúng phân tách một chức năng thành nhiều phần hơn.

Currying phân tách một hàm của nhiều đối số thành các hàm lồng nhau của các đối số đơn trả về các hàm của các đối số đơn. Không có điểm nào trong việc giới thiệu chức năng của một hoặc ít đối số, vì nó không có ý nghĩa.

Ứng dụng một phần phân rã một hàm của nhiều đối số thành một hàm của các đối số nhỏ hơn mà hiện tại các đối số bị thiếu đã được thay thế cho giá trị được cung cấp.

Việc đóng cửa phân tách một hàm thành một hàm và một tập dữ liệu trong đó các biến bên trong hàm không được truyền vào có thể nhìn vào bên trong tập dữ liệu để tìm giá trị để liên kết khi được yêu cầu đánh giá.

Điều khó hiểu về tất cả những điều này là chúng có thể được sử dụng để thực hiện một tập hợp con khác. Vì vậy, về bản chất, tất cả chúng là một chút chi tiết thực hiện. Tất cả chúng đều cung cấp giá trị tương tự ở chỗ bạn không cần phải thu thập tất cả các giá trị trả trước và trong đó bạn có thể sử dụng lại một phần của hàm, vì bạn đã phân tách nó thành các đơn vị kín đáo.

Tiết lộ

Tôi không phải là một chuyên gia về chủ đề này, gần đây tôi mới bắt đầu tìm hiểu về những điều này, và vì vậy tôi cung cấp sự hiểu biết hiện tại của mình, nhưng nó có thể có những lỗi mà tôi mời bạn chỉ ra, và tôi sẽ sửa như / nếu Tôi khám phá bất kỳ.


1
Vậy câu trả lời là: cà ri có lợi thế không?
ceving

1
@ceving Theo tôi biết, điều đó đúng. Trong thực tế cà ri và ứng dụng một phần sẽ cung cấp cho bạn những lợi ích tương tự. Lựa chọn thực hiện trong một ngôn ngữ được thực hiện vì lý do thực hiện, người ta có thể dễ dàng thực hiện hơn một ngôn ngữ nhất định.
Didier A.

5

Currying (ứng dụng một phần) cho phép bạn tạo một chức năng mới từ một chức năng hiện có bằng cách sửa một số tham số. Đây là một trường hợp đặc biệt của việc đóng từ vựng trong đó hàm ẩn danh chỉ là một trình bao bọc tầm thường chuyển một số đối số đã bắt sang hàm khác. Chúng ta cũng có thể làm điều này bằng cách sử dụng cú pháp chung để thực hiện các đóng từ vựng, nhưng ứng dụng một phần cung cấp một đường cú pháp đơn giản hóa.

Đây là lý do tại sao các lập trình viên Lisp, khi làm việc theo kiểu chức năng, đôi khi sử dụng các thư viện cho ứng dụng một phần .

Thay vì (lambda (x) (+ 3 x)), cung cấp cho chúng ta một hàm thêm 3 vào đối số của nó, bạn có thể viết một cái gì đó giống như vậy (op + 3), và vì vậy để thêm 3 vào mỗi phần tử của một danh sách nào đó thì sẽ (mapcar (op + 3) some-list)thay vì (mapcar (lambda (x) (+ 3 x)) some-list). Đây opvĩ mô sẽ làm cho bạn một chức năng mà phải mất một số đối số x y z ...và gọi (+ a x y z ...).

Trong nhiều ngôn ngữ chức năng thuần túy, ứng dụng một phần đã ăn sâu vào cú pháp để không có optoán tử. Để kích hoạt ứng dụng một phần, bạn chỉ cần gọi một hàm có ít đối số hơn yêu cầu. Thay vì tạo ra một "insufficient number of arguments"lỗi, kết quả là một hàm của các đối số còn lại.


"Currying ... cho phép bạn tạo một hàm mới ... bằng cách sửa một số tham số" - không, một hàm loại a -> b -> ckhông có tham số s (số nhiều), nó chỉ có một tham số , c. Khi được gọi, nó trả về một hàm kiểu a -> b.
Max Heiber

4

Đối với chức năng

fun add(x, y) = x + y

Nó là của hình thức f': 'a * 'b -> 'c

Để đánh giá người ta sẽ làm

add(3, 5)
val it = 8 : int

Đối với chức năng curried

fun add x y = x + y

Để đánh giá người ta sẽ làm

add 3
val it = fn : int -> int

Trong đó nó là một tính toán một phần, cụ thể (3 + y), sau đó người ta có thể hoàn thành tính toán với

it 5
val it = 8 : int

thêm vào trong trường hợp thứ hai là của mẫu f: 'a -> 'b -> 'c

Điều mà currying đang làm ở đây là chuyển đổi một hàm có hai thỏa thuận thành một mà chỉ mất một kết quả trả về. Đánh giá một phần

Tại sao một người cần điều này?

Nói xtrên RHS không chỉ là một int thông thường, mà thay vào đó là một tính toán phức tạp cần một chút thời gian để hoàn thành, vì sự gia tăng, vì lợi ích, hai giây.

x = twoSecondsComputation(z)

Vì vậy, chức năng bây giờ trông giống như

fun add (z:int) (y:int) : int =
    let
        val x = twoSecondsComputation(z)
    in
        x + y
    end;

Loại add : int * int -> int

Bây giờ chúng tôi muốn tính toán hàm này cho một dãy số, hãy ánh xạ nó

val result1 = map (fn x => add (20, x)) [3, 5, 7];

Đối với các kết quả trên twoSecondsComputationđược đánh giá mỗi lần. Điều này có nghĩa là phải mất 6 giây cho tính toán này.

Sử dụng kết hợp dàn dựng và cà ri người ta có thể tránh điều này.

fun add (z:int) : int -> int =
    let
        val x = twoSecondsComputation(z)
    in
        (fn y => x + y)
    end;

Của hình thức cà ri add : int -> int -> int

Bây giờ người ta có thể làm,

val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];

Các twoSecondsComputationnhu cầu chỉ được đánh giá một lần. Để tăng tỷ lệ, thay hai giây bằng 15 phút hoặc bất kỳ giờ nào, sau đó có bản đồ với 100 số.

Tóm tắt : Currying là tuyệt vời khi sử dụng với các phương pháp khác cho các chức năng cấp cao hơn như một công cụ đánh giá một phần. Mục đích của nó không thể thực sự được chứng minh bởi chính nó.


3

Currying cho phép thành phần chức năng linh hoạt.

Tôi tạo thành một chức năng "cà ri". Trong bối cảnh này, tôi không quan tâm loại logger nào tôi nhận được hoặc nó đến từ đâu. Tôi không quan tâm hành động đó là gì hoặc đến từ đâu. Tất cả tôi quan tâm là xử lý đầu vào của tôi.

var builder = curry(function(input, logger, action) {
     logger.log("Starting action");
     try {
         action(input);
         logger.log("Success!");
     }
     catch (err) {
         logger.logerror("Boo we failed..", err);
     }
});
var x = "My input.";
goGatherArgs(builder)(x); // Supplies action first, then logger somewhere.

Biến xây dựng là một hàm trả về một hàm trả về một hàm lấy đầu vào của tôi thực hiện công việc của tôi. Đây là một ví dụ hữu ích đơn giản và không phải là một đối tượng trong tầm nhìn.


2

Currying là một lợi thế khi bạn không có tất cả các đối số cho một chức năng. Nếu bạn tình cờ đánh giá đầy đủ chức năng, thì không có sự khác biệt đáng kể nào.

Currying cho phép bạn tránh đề cập đến các thông số chưa cần thiết. Nó ngắn gọn hơn và không yêu cầu tìm tên tham số không va chạm với một biến khác trong phạm vi (đó là lợi ích yêu thích của tôi).

Ví dụ: khi sử dụng các hàm lấy các hàm làm đối số, bạn sẽ thường thấy mình trong các tình huống bạn cần các hàm như "thêm 3 vào đầu vào" hoặc "so sánh đầu vào với biến v". Với currying, các chức năng này dễ dàng được viết: add 3(== v). Không có cà ri, bạn phải sử dụng biểu thức lambda: x => add 3 xx => x == v. Các biểu thức lambda dài gấp đôi và có một lượng nhỏ công việc bận rộn liên quan đến việc chọn tên bên cạnh xnếu đã có xphạm vi.

Một lợi ích phụ của các ngôn ngữ dựa trên currying là, khi viết mã chung cho các hàm, bạn không kết thúc với hàng trăm biến thể dựa trên số lượng tham số. Ví dụ: trong C #, phương thức 'cà ri' sẽ cần các biến thể cho Func <R>, Func <A, R>, Func <A1, A2, R>, Func <A1, A2, A3, R>, v.v. mãi mãi. Trong Haskell, tương đương với Func <A1, A2, R> giống với Func <Tuple <A1, A2>, R> hoặc Func <A1, Func <A2, R >> (và Func <R> giống với Func <Đơn vị, R>), vì vậy tất cả các biến thể tương ứng với trường hợp Func <A, R> đơn.


2

Lý do chính mà tôi có thể nghĩ đến (và tôi không phải là chuyên gia về chủ đề này bằng mọi cách) bắt đầu cho thấy lợi ích của nó khi các chức năng chuyển từ tầm thường sang không tầm thường. Trong tất cả các trường hợp tầm thường với hầu hết các khái niệm về bản chất này, bạn sẽ không tìm thấy lợi ích thực sự. Tuy nhiên, hầu hết các ngôn ngữ chức năng sử dụng rất nhiều ngăn xếp trong các hoạt động xử lý. Hãy xem PostScript hoặc Lisp là ví dụ về điều này. Bằng cách sử dụng currying, các chức năng có thể được xếp chồng hiệu quả hơn và lợi ích này trở nên rõ ràng khi các hoạt động phát triển ngày càng ít hơn. Theo cách thức bị quấy rối, lệnh và đối số có thể được ném trên ngăn xếp theo thứ tự và bật ra khi cần thiết để chúng được chạy theo đúng thứ tự.


1
Làm thế nào chính xác đòi hỏi nhiều khung stack hơn được tạo ra làm cho mọi thứ hiệu quả hơn?
Mason Wheeler

1
@MasonWheeler: Tôi không biết, vì tôi đã nói tôi không phải là chuyên gia về ngôn ngữ chức năng hay đặc biệt là cà ri. Tôi dán nhãn wiki cộng đồng này đặc biệt vì điều đó.
Joel Etherton

4
@MasonWheeler Bạn có một điểm để viết cụm từ của câu trả lời này, nhưng hãy để tôi nói và nói rằng số lượng khung stack thực sự được tạo ra phụ thuộc rất nhiều vào việc thực hiện. Ví dụ, trong máy G không có thẻ không có spin (STG; cách GHC thực hiện Haskell) sẽ trì hoãn việc đánh giá thực tế cho đến khi nó tích lũy tất cả (hoặc ít nhất là nhiều như nó yêu cầu). Tôi dường như không thể nhớ lại liệu điều này được thực hiện cho tất cả các chức năng hay chỉ cho các nhà xây dựng, nhưng tôi nghĩ rằng nó phải có khả năng cho hầu hết các chức năng. (Sau đó, một lần nữa, khái niệm "khung ngăn xếp" không thực sự áp dụng cho STG.)

1

Currying phụ thuộc rất nhiều (thậm chí dứt khoát) vào khả năng trả về một chức năng.

Xem xét mã giả (giả) này.

var f = (m, x, b) => ... trả lại một cái gì đó ...

Hãy quy định rằng việc gọi f với ít hơn ba đối số sẽ trả về một hàm.

var g = f (0, 1); // điều này trả về một hàm bị ràng buộc bởi 0 và 1 (m và x) chấp nhận thêm một đối số (b).

var y = g (42); // gọi g với đối số thứ ba bị thiếu, sử dụng 0 và 1 cho m và x

Rằng bạn có thể áp dụng một phần các đối số và lấy lại hàm có thể sử dụng lại (ràng buộc với các đối số mà bạn đã cung cấp) khá hữu ích (và DRY).

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.