Có gì đặc biệt về cà ri hoặc ứng dụng một phần?


9

Tôi đã đọc các bài viết về lập trình chức năng hàng ngày và đang cố gắng áp dụng một số thực tiễn càng nhiều càng tốt. Nhưng tôi không hiểu những gì là duy nhất trong cà ri hoặc ứng dụng một phần.

Lấy mã Groovy này làm ví dụ:

def mul = { a, b -> a * b }
def tripler1 = mul.curry(3)
def tripler2 = { mul(3, it) }

Tôi không hiểu sự khác biệt giữa tripler1tripler2. Cả hai không giống nhau sao? 'Cà ri' được hỗ trợ bằng các ngôn ngữ chức năng thuần túy hoặc một phần như Groovy, Scala, Haskell, v.v. hàm hoặc đóng sẽ chuyển tiếp các tham số đến hàm ban đầu (như tripler2) trong hầu hết các ngôn ngữ (thậm chí C.)

Am i thiếu cái gì ở đây? Có những nơi tôi có thể sử dụng ứng dụng currying và một phần trong ứng dụng Grails của mình nhưng tôi ngần ngại làm điều đó bởi vì tôi đang tự hỏi mình "Nó khác nhau như thế nào?"

Vui lòng làm sáng tỏ cho tôi.

EDIT: Các bạn có nói rằng ứng dụng / currying một phần đơn giản là hiệu quả hơn so với việc tạo / gọi một hàm khác chuyển các tham số mặc định thành hàm gốc?


1
Ai đó có thể vui lòng tạo các thẻ "cà ri" hoặc "cà ri" không?
Vigneshwaran

Làm thế nào để bạn cà ri trong C?
Giorgio

đây có lẽ là thực sự nhiều hơn về các lập trình viên
phần.stackexchange.com / questions / 152868 / Fiêu

1
@Vigneshwaran: AFAIK bạn không cần tạo một chức năng khác bằng ngôn ngữ hỗ trợ cà ri. Ví dụ, trong Haskell f x y = x + ycó nghĩa flà một hàm lấy một tham số int. Kết quả của f x( fáp dụng cho x) là một hàm có một tham số int. Kết quả f x y(hoặc (f x) y, tức là f xđược áp dụng cho y) là một biểu thức không có tham số đầu vào và được đánh giá bằng cách giảm x + y.
Giorgio

1
Bạn có thể đạt được những điều tương tự, nhưng số lượng nỗ lực bạn trải qua với C đau đớn hơn nhiều và không hiệu quả như trong một ngôn ngữ như haskell trong đó đó là hành vi mặc định
Daniel Gratzer

Câu trả lời:


8

Currying là về việc biến / biểu diễn một hàm trong đó có n đầu vào thành n hàm mà mỗi hàm lấy 1 đầu vào. Ứng dụng một phần là về việc sửa một số đầu vào cho một chức năng.

Động lực cho ứng dụng một phần chủ yếu là nó giúp việc viết các thư viện hàm bậc cao dễ dàng hơn. Ví dụ, các thuật toán trong C ++ STL hầu hết đều sử dụng các biến vị ngữ hoặc hàm unary, bind1st cho phép người dùng thư viện móc vào các hàm không đơn nguyên có giá trị bị ràng buộc. Do đó, người viết thư viện không cần cung cấp các hàm quá tải cho tất cả các thuật toán có các hàm đơn nguyên để cung cấp các phiên bản nhị phân

Bản thân Currying rất hữu ích vì nó cung cấp cho bạn một phần ứng dụng bất cứ nơi nào bạn muốn nó miễn phí, tức là bạn không còn cần một chức năng như bind1stáp dụng một phần.


curryingmột cái gì đó cụ thể để Groovy hoặc áp dụng trên các ngôn ngữ?
lưỡng cư

@foampile một cái gì đó nó có thể áp dụng trên ngôn ngữ, nhưng trớ trêu thay cà ri groovy không thực sự làm điều đó programmers.stackexchange.com/questions/152868/...
jk.

@jk. Bạn đang nói rằng ứng dụng currying / một phần hiệu quả hơn so với việc tạo và gọi một chức năng khác?
Vigneshwaran

2
@Vigneshwaran - nó không nhất thiết phải hiệu quả hơn, nhưng nó chắc chắn hiệu quả hơn về mặt thời gian của lập trình viên. Cũng lưu ý rằng trong khi currying được hỗ trợ bởi nhiều ngôn ngữ chức năng, nhưng thường không được hỗ trợ trong OO hoặc ngôn ngữ thủ tục. (Hoặc ít nhất, không phải bằng chính ngôn ngữ.)
Stephen C

6

Nhưng tôi có thể làm điều tương tự (cà ri trái, cà ri phải, cà ri n hoặc ứng dụng một phần) bằng cách đơn giản tạo một hàm hoặc tên ẩn danh khác hoặc chuyển tiếp các tham số đến hàm ban đầu (như tripler2) trong hầu hết các ngôn ngữ ( thậm chí C.)

Và trình tối ưu hóa sẽ xem xét điều đó và nhanh chóng đi đến một cái gì đó mà nó có thể hiểu được. Currying là một mẹo nhỏ hay cho người dùng cuối, nhưng có lợi ích tốt hơn nhiều từ quan điểm thiết kế ngôn ngữ. Đó là thực sự tốt đẹp để xử lý tất cả các phương pháp như unary A -> Bnơi Bcó thể phương pháp khác.

Nó đơn giản hóa những phương thức bạn phải viết để xử lý các hàm bậc cao hơn. Phân tích tĩnh và tối ưu hóa trong ngôn ngữ của bạn chỉ có một đường dẫn để làm việc với hành vi đó theo cách đã biết. Liên kết tham số chỉ rơi ra khỏi thiết kế chứ không yêu cầu hoops thực hiện hành vi phổ biến này.


6

Như @jk. ám chỉ, cà ri có thể giúp làm cho mã tổng quát hơn.

Ví dụ: giả sử bạn có ba chức năng này (trong Haskell):

> let q a b = (2 + a) * b

> let r g = g 3

> let f a b = b (a 1)

Hàm fở đây nhận hai hàm làm đối số, chuyển 1đến hàm thứ nhất và chuyển kết quả của lệnh gọi đầu tiên sang hàm thứ hai.

Nếu chúng ta gọi fbằng cách sử dụng qrlàm đối số, thì nó thực sự sẽ được thực hiện:

> r (q 1)

nơi qsẽ được áp dụng 1và trả về một chức năng khác (như qđược chế biến); hàm trả về này sau đó sẽ được chuyển đến rlàm đối số của nó để được đưa ra một đối số 3. Kết quả của điều này sẽ là một giá trị của 9.

Bây giờ, giả sử chúng ta có hai chức năng khác:

> let s a = 3 * a

> let t a = 4 + a

chúng ta cũng có thể chuyển những thứ này đến fvà nhận được giá trị 7hoặc 15, tùy thuộc vào việc các đối số của chúng ta là s thay t s. Vì các hàm này đều trả về một giá trị thay vì một hàm, nên không có ứng dụng một phần nào diễn ra trong f s thoặc f t s.

Nếu chúng ta đã viết fvới qrtrong tâm trí chúng ta có thể đã sử dụng một lambda (chức năng ẩn danh) thay vì áp dụng một phần, ví dụ như:

> let f' a b = b (\x -> a 1 x)

nhưng điều này sẽ hạn chế tính tổng quát của f'. fcó thể được gọi với các đối số qrhoặc st, nhưng f'chỉ có thể được gọi với qr- f' s tf' t scả hai đều dẫn đến một lỗi.

HƠN

Nếu f'được gọi với một q'/ r'cặp trong đó q'lấy hơn hai đối số, q'thì cuối cùng vẫn sẽ được áp dụng một phần f'.

Ngoài ra, bạn có thể quấn qbên ngoài fthay vì bên trong, nhưng điều đó sẽ để lại cho bạn một lambda lồng nhau khó chịu:

f (\x -> (\y -> q x y)) r

đó là cơ bản những gì qđã được curried ở nơi đầu tiên!


Bạn mở mắt ra. Câu trả lời của bạn khiến tôi nhận ra các hàm được áp dụng / được áp dụng một phần khác với việc tạo một hàm mới chuyển các đối số cho hàm ban đầu như thế nào. 1. Chuyển qua các hàm curryied / paed (như f (q.curry (2)) gọn gàng hơn so với việc tạo các hàm riêng biệt không cần thiết chỉ để sử dụng tạm thời. (Trong các ngôn ngữ chức năng như
Groovy

2. Trong câu hỏi của tôi, tôi nói, "Tôi có thể làm tương tự ở C." Có, nhưng trong các ngôn ngữ phi chức năng, nơi bạn không thể chuyển các hàm dưới dạng dữ liệu, tạo một hàm riêng biệt, chuyển các tham số thành bản gốc, không có tất cả lợi ích của currying / pa
Vigneshwaran

Tôi nhận thấy rằng Groovy không hỗ trợ kiểu tổng quát mà Haskell hỗ trợ. Tôi đã phải viết def f = { a, b -> b a.curry(1) }để làm cho f q, rcông việc và def f = { a, b -> b a(1) }hoặc def f = { a, b -> b a.curry(1)() }cho f s, tlại làm việc. Bạn phải vượt qua tất cả các tham số hoặc nói rõ ràng là bạn đang cà ri. :(
Vigneshwaran

2
@Vigneshwaran: Vâng, thật an toàn khi nói rằng Haskell và cà ri kết hợp với nhau rất tốt . ;] Lưu ý rằng trong Haskell, các hàm được xử lý (theo định nghĩa chính xác) theo mặc định và khoảng trắng biểu thị ứng dụng hàm, vì vậy f x ycó nghĩa là nhiều ngôn ngữ sẽ viết f(x)(y), không phải f(x, y). Có lẽ mã của bạn sẽ hoạt động trong Groovy nếu bạn viết qđể nó được gọi là như thế q(1)(2)nào?
CA McCann

1
@Vigneshwaran Vui mừng tôi có thể giúp! Tôi cảm thấy nỗi đau của bạn về việc phải nói rõ ràng rằng bạn đang làm một phần ứng dụng. Trong Clojure tôi phải làm (partial f a b ...)- đã quen với Haskell, tôi rất nhớ việc nấu cà ri thích hợp khi lập trình bằng các ngôn ngữ khác (mặc dù gần đây tôi đã làm việc trong F #, rất may là hỗ trợ nó).
paul

3

Có hai điểm chính về ứng dụng một phần. Đầu tiên là cú pháp / tiện lợi - một số định nghĩa trở nên dễ dàng hơn và ngắn hơn để đọc và viết, như @jk đã đề cập. (Kiểm tra lập trình Pointfree để biết thêm về mức độ tuyệt vời của nó!)

Thứ hai, như @telastyn đã đề cập, là về một mô hình chức năng và không chỉ đơn thuần là thuận tiện. Trong phiên bản Haskell, từ đó tôi sẽ lấy ví dụ của mình vì tôi không quen với các ngôn ngữ khác với ứng dụng một phần, tất cả các hàm chỉ có một đối số. Có, thậm chí các chức năng như:

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

lấy một đối số duy nhất; bởi vì tính kết hợp của hàm tạo kiểu hàm ->, ở trên tương đương với:

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

đó là một hàm lấy avà trả về một hàm [a] -> [a].

Điều này cho phép chúng ta viết các hàm như:

($) :: (a -> b) -> a -> b

có thể áp dụng bất kỳ hàm nào cho một đối số của loại thích hợp. Ngay cả những người điên như:

f :: (t, t1) -> t -> t1 -> (t2 -> t3 -> (t, t1)) -> t2 -> t3 -> [(t, t1)]
f q r s t u v = q : (r, s) : [t u v]

f' :: () -> Char -> (t2 -> t3 -> ((), Char)) -> t2 -> t3 -> [((), Char)]
f' = f $ ((), 'a')  -- <== works fine

Được rồi, đó là một ví dụ giả định. Nhưng một cách hữu ích hơn liên quan đến lớp Loại ứng dụng , bao gồm phương thức này:

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Như bạn có thể thấy, kiểu này giống hệt như $nếu bạn lấy đi Applicative fbit và trên thực tế, lớp này mô tả ứng dụng hàm trong ngữ cảnh. Vì vậy, thay vì ứng dụng chức năng bình thường:

ghci> map (+3) [1..5]  
[4,5,6,7,8]

Chúng ta có thể áp dụng các hàm trong ngữ cảnh Áp dụng; ví dụ: trong bối cảnh Có thể trong đó một cái gì đó có thể là hiện tại hoặc thiếu:

ghci> Just map <*> Just (+3) <*> Just [1..5]
Just [4,5,6,7,8]

ghci> Just map <*> Nothing <*> Just [1..5]
Nothing

Bây giờ phần thực sự thú vị là lớp Loại ứng dụng không đề cập bất cứ điều gì về các hàm của nhiều hơn một đối số - tuy nhiên, nó có thể xử lý chúng, thậm chí là các hàm của 6 đối số như f:

fA' :: Maybe (() -> Char -> (t2 -> t3 -> ((), Char)) -> t2 -> t3 -> [((), Char)])
fA' = Just f <*> Just ((), 'a')

Theo như tôi biết, lớp Loại ứng dụng ở dạng chung sẽ không thể thực hiện được nếu không có một số khái niệm về ứng dụng một phần. (Đối với bất kỳ chuyên gia lập trình nào ngoài đó - vui lòng sửa lỗi cho tôi nếu tôi sai!) Tất nhiên, nếu ngôn ngữ của bạn thiếu ứng dụng một phần, bạn có thể xây dựng nó ở một dạng nào đó, nhưng ... nó không giống nhau ? :)


1
Applicativemà không sử dụng currying hoặc một phần ứng dụng sẽ sử dụng fzip :: (f a, f b) -> f (a, b). Trong một ngôn ngữ có các hàm bậc cao hơn, điều này cho phép bạn nâng ứng dụng currying và một phần vào ngữ cảnh của functor và tương đương với (<*>). Nếu không có các hàm bậc cao hơn, bạn sẽ không có fmapnên toàn bộ mọi thứ sẽ trở nên vô dụng.
CA McCann

@CAMcCann cảm ơn bạn đã phản hồi! Tôi biết tôi đã ở trên đầu với câu trả lời này. Vì vậy, những gì tôi nói sai?

1
Đó là chính xác về tinh thần, chắc chắn. Tách tóc theo các định nghĩa về "hình thức chung", "có thể" và có "khái niệm ứng dụng một phần" sẽ không thay đổi thực tế đơn giản rằng f <$> x <*> yphong cách thành ngữ duyên dáng hoạt động dễ dàng vì ứng dụng cà ri và ứng dụng một phần dễ dàng. Nói cách khác, những gì dễ chịu quan trọng hơn những gì có thể ở đây.
CA McCann

Mỗi lần tôi nhìn thấy các ví dụ mã về lập trình chức năng, tôi tin chắc hơn đó là một trò đùa phức tạp và nó không tồn tại.
Kieveli

1
@Kieveli thật đáng tiếc khi bạn cảm thấy như vậy. Có rất nhiều hướng dẫn tốt ngoài đó sẽ giúp bạn đứng dậy và chạy với sự hiểu biết tốt về những điều cơ bả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.