Apa yang istimewa tentang aplikasi kari atau sebagian?

9

Saya telah membaca artikel tentang pemrograman Fungsional setiap hari dan berusaha menerapkan beberapa praktik sebanyak mungkin. Tapi saya tidak mengerti apa yang unik dalam aplikasi kari atau aplikasi parsial.

Ambil kode Groovy ini sebagai contoh:

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

Saya tidak mengerti apa perbedaan antara tripler1dan tripler2. Bukankah keduanya sama? 'Currying' didukung dalam bahasa fungsional murni atau parsial seperti Groovy, Scala, Haskell dll. Tapi saya dapat melakukan hal yang sama (kari-kiri, kari-kanan, kari-n, atau aplikasi sebagian) dengan hanya membuat nama lain atau anonim fungsi atau penutupan yang akan meneruskan parameter ke fungsi asli (seperti tripler2) di sebagian besar bahasa (bahkan C.)

Apakah saya melewatkan sesuatu di sini? Ada tempat-tempat di mana saya dapat menggunakan aplikasi currying dan parsial dalam aplikasi Grails saya tetapi saya ragu untuk melakukannya karena saya bertanya pada diri sendiri "Apa bedanya?"

Tolong beri tahu saya.

EDIT: Apakah kalian mengatakan bahwa aplikasi parsial / currying hanya lebih efisien daripada membuat / memanggil fungsi lain yang meneruskan parameter default ke fungsi asli?

Vigneshwaran
sumber
1
Bisakah seseorang membuat tag "curry" atau "currying"?
Vigneshwaran
Bagaimana Anda menjilat C?
Giorgio
ini mungkin benar-benar tentang programer
jk.
1
@ Vigneshwaran: AFAIK Anda tidak perlu membuat fungsi lain dalam bahasa yang mendukung currying. Misalnya, dalam Haskell f x y = x + yberarti itu fadalah fungsi yang mengambil satu parameter int. Hasil dari f x( fditerapkan ke x) adalah fungsi yang mengambil satu parameter int. Hasilnya f x y(atau (f x) y, yaitu f xditerapkan ke y) adalah ekspresi yang tidak mengambil parameter input dan dievaluasi dengan mengurangi x + y.
Giorgio
1
Anda dapat mencapai hal-hal yang sama, tetapi jumlah usaha yang Anda lalui dengan C jauh lebih menyakitkan dan tidak seefisien dalam bahasa seperti haskell di mana itu adalah perilaku default
Daniel Gratzer

Jawaban:

8

Currying adalah tentang memutar / mewakili suatu fungsi yang mengambil n input menjadi n fungsi yang masing-masing mengambil 1 input. Aplikasi parsial adalah tentang memperbaiki beberapa input ke suatu fungsi.

Motivasi untuk aplikasi parsial terutama yang membuatnya lebih mudah untuk menulis perpustakaan fungsi tingkat tinggi. Sebagai contoh, algoritma dalam C ++ STL sebagian besar mengambil predikat atau fungsi unary, bind1st memungkinkan pengguna perpustakaan untuk mengaitkan fungsi non unary dengan batas nilai. Penulis perpustakaan karenanya tidak perlu menyediakan fungsi berlebih untuk semua algoritma yang mengambil fungsi unary untuk menyediakan versi biner

Currying itu sendiri berguna karena memberi Anda sebagian aplikasi di mana saja Anda inginkan secara gratis yaitu Anda tidak lagi memerlukan fungsi seperti bind1stuntuk menerapkan sebagian.

jk.
sumber
apakah ada curryingsesuatu yang spesifik untuk asyik atau berlaku di berbagai bahasa?
amfibi
@foampile sesuatu yang dapat diterapkan lintas bahasa, tetapi ironisnya kari asyik tidak benar-benar melakukannya programmer.stackexchange.com/questions/152868/…
jk.
@jk. Apakah Anda mengatakan bahwa aplikasi currying / parsial lebih efisien daripada membuat dan memanggil fungsi lain?
Vigneshwaran
2
@ Vigneshwaran - ini tidak selalu lebih berkinerja, tetapi jelas lebih efisien dalam hal waktu programmer. Perhatikan juga bahwa sementara kari didukung oleh banyak bahasa fungsional, tetapi umumnya tidak didukung dalam bahasa OO atau prosedural. (Atau setidaknya, bukan oleh bahasa itu sendiri.)
Stephen C
6

Tetapi saya dapat melakukan hal yang sama (kari-kiri, kari-kanan, aplikasi-kari n atau sebagian) dengan hanya membuat fungsi atau penutupan bernama atau anonim yang akan meneruskan parameter ke fungsi asli (seperti tripler2) dalam kebanyakan bahasa ( bahkan C.)

Dan pengoptimal akan melihat itu dan segera pergi ke sesuatu yang dapat dimengerti. Currying adalah trik kecil yang bagus untuk pengguna akhir, tetapi memiliki manfaat yang jauh lebih baik dari sudut pandang desain bahasa. Ini benar-benar bagus untuk menangani semua metode yang unary A -> Bmana Bmungkin metode lain.

Ini menyederhanakan metode apa yang harus Anda tulis untuk menangani fungsi tingkat tinggi. Analisis statis dan pengoptimalan dalam bahasa hanya memiliki satu jalur untuk bekerja dengan yang berperilaku dengan cara yang diketahui. Penjilidan parameter hanya jatuh dari desain daripada membutuhkan simpai untuk melakukan perilaku umum ini.

Telastyn
sumber
6

Sebagai @jk. disinggung, kari dapat membantu membuat kode lebih umum.

Sebagai contoh, misalkan Anda memiliki tiga fungsi ini (dalam Haskell):

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

> let r g = g 3

> let f a b = b (a 1)

Fungsi di fsini mengambil dua fungsi sebagai argumen, beralih 1ke fungsi pertama dan meneruskan hasil panggilan pertama ke fungsi kedua.

Jika kami akan menelepon fmenggunakan qdan rsebagai argumen, itu akan efektif dilakukan:

> r (q 1)

di mana qakan diterapkan ke 1dan mengembalikan fungsi lain (seperti qyang kari); fungsi yang dikembalikan ini kemudian akan diteruskan ke rsebagai argumen untuk diberikan argumen 3. Hasil ini akan menjadi nilai 9.

Sekarang, katakanlah kita memiliki dua fungsi lain:

> let s a = 3 * a

> let t a = 4 + a

kami dapat meneruskannya fjuga dan mendapatkan nilai 7atau 15, tergantung pada apakah argumen kami s tatau t s. Karena fungsi-fungsi ini keduanya mengembalikan nilai daripada fungsi, tidak ada aplikasi parsial yang akan terjadi di f s tatau f t s.

Jika kami telah menulis fdengan qdan rmengingat kami mungkin telah menggunakan lambda (fungsi anonim) daripada aplikasi parsial, misalnya:

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

tetapi ini akan membatasi generalisasi f'. fdapat dipanggil dengan argumen qdan ratau sdan t, tetapi f'hanya bisa dipanggil dengan qdan r- f' s tdan f' t skeduanya menghasilkan kesalahan.

LEBIH

Jika f'dipanggil dengan a q'/ r'pair di mana q'mengambil lebih dari dua argumen, q'masih akan sebagian diterapkan di f'.

Atau, Anda bisa membungkus di qluar, fbukan di dalam, tapi itu akan meninggalkan Anda dengan lambda bersarang jahat:

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

yang pada dasarnya adalah kari qitu sejak awal!

paul
sumber
Anda membuka mata saya. Jawaban Anda membuat saya menyadari bagaimana fungsi curried / sebagian diterapkan berbeda dari membuat fungsi baru yang meneruskan argumen ke fungsi asli. 1. Melewati fungsi curryied / paed (seperti f (q.curry (2)) lebih rapi daripada membuat fungsi terpisah yang tidak perlu hanya untuk penggunaan sementara. (Dalam bahasa fungsional seperti asyik)
Vigneshwaran
2. Dalam pertanyaan saya, saya berkata, "Saya bisa melakukan hal yang sama dalam C." Ya, tetapi dalam bahasa non-fungsional, di mana Anda tidak dapat menyerahkan fungsi sebagai data, membuat fungsi terpisah, yang meneruskan parameter ke asli, tidak memiliki semua manfaat currying / pa
Vigneshwaran
Saya perhatikan bahwa Groovy tidak mendukung jenis generalisasi yang didukung Haskell. Saya harus menulis def f = { a, b -> b a.curry(1) }agar f q, rbekerja dan def f = { a, b -> b a(1) }atau def f = { a, b -> b a.curry(1)() }untuk f s, tbekerja. Anda harus melewati semua parameter atau secara eksplisit mengatakan Anda sedang mencari. :(
Vigneshwaran
2
@ Vigneshwaran: Ya, aman untuk mengatakan bahwa Haskell dan kari berjalan bersama dengan sangat baik . ;] Perhatikan bahwa di Haskell, fungsi-fungsi dilengkungkan (dalam definisi yang benar) secara default dan spasi putih menunjukkan aplikasi fungsi, jadi f x yberarti apa yang akan ditulis oleh banyak bahasa f(x)(y), bukan f(x, y). Mungkin kode Anda akan berfungsi di Groovy jika Anda menulis qsehingga diharapkan seperti itu q(1)(2)?
CA McCann
1
@ Vigneshwaran Senang saya bisa membantu! Saya merasakan sakit Anda karena harus secara eksplisit mengatakan Anda sedang melakukan aplikasi parsial. Di Clojure yang harus saya lakukan (partial f a b ...)- terbiasa dengan Haskell, saya kehilangan banyak currying yang tepat ketika pemrograman dalam bahasa lain (meskipun saya baru-baru ini bekerja di F # yang, untungnya, mendukungnya).
paul
3

Ada dua poin utama tentang aplikasi parsial. Yang pertama adalah sintaksis / kenyamanan - beberapa definisi menjadi lebih mudah dan lebih pendek untuk dibaca dan ditulis, seperti yang disebutkan oleh @jk. (Lihat pemrograman Pointfree untuk informasi lebih lanjut tentang betapa hebatnya ini!)

Yang kedua, seperti yang disebutkan @telastyn, adalah tentang model fungsi dan tidak hanya nyaman. Dalam versi Haskell, dari mana saya akan mendapatkan contoh saya karena saya tidak terbiasa dengan bahasa lain dengan aplikasi parsial, semua fungsi mengambil satu argumen. Ya, bahkan fungsinya seperti:

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

ambil satu argumen; karena asosiatif dari konstruktor tipe fungsi ->, hal di atas setara dengan:

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

yang merupakan fungsi yang mengambil adan mengembalikan fungsi [a] -> [a].

Ini memungkinkan kita untuk menulis fungsi seperti:

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

yang dapat menerapkan fungsi apa pun pada argumen dengan tipe yang sesuai. Bahkan yang gila seperti:

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

Oke, jadi itu contoh yang dibuat-buat. Tetapi yang lebih bermanfaat melibatkan kelas tipe Applicative , yang mencakup metode ini:

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

Seperti yang Anda lihat, tipe ini identik dengan $jika Anda mengambil Applicative fbitnya, dan pada kenyataannya, kelas ini menjelaskan aplikasi fungsi dalam suatu konteks. Jadi alih-alih aplikasi fungsi normal:

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

Kami dapat menerapkan fungsi dalam konteks Applicative; misalnya, dalam konteks Maybe di mana sesuatu dapat hadir atau hilang:

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

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

Sekarang bagian yang sangat keren adalah bahwa kelas tipe Applicative tidak menyebutkan apa pun tentang fungsi lebih dari satu argumen - namun, ia dapat menangani mereka, bahkan fungsi 6 argumen seperti f:

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

Sejauh yang saya tahu, kelas tipe Applicative dalam bentuk umumnya tidak akan mungkin terjadi tanpa konsepsi aplikasi parsial. (Untuk para ahli pemrograman di luar sana - tolong perbaiki saya jika saya salah!) Tentu saja, jika bahasa Anda tidak memiliki aplikasi parsial, Anda dapat membangunnya dalam beberapa bentuk, tapi ... itu tidak sama, bukan? ? :)


sumber
1
Applicativetanpa currying atau aplikasi parsial akan digunakan fzip :: (f a, f b) -> f (a, b). Dalam bahasa dengan fungsi tingkat tinggi, ini memungkinkan Anda mengangkat aplikasi sebagian dan currying ke dalam konteks functor dan setara dengan (<*>). Tanpa fungsi tingkat tinggi Anda tidak akan memiliki fmapsehingga semuanya akan sia-sia.
CA McCann
@CAMcCann terima kasih atas umpan baliknya! Saya tahu saya berada di atas kepala saya dengan jawaban ini. Jadi apa yang saya katakan salah?
1
Itu benar dalam roh, tentu saja. Memisahkan rambut dari definisi "bentuk umum", "mungkin", dan memiliki "konsepsi aplikasi parsial" tidak akan mengubah fakta sederhana bahwa f <$> x <*> ygaya idiomatik yang menawan bekerja dengan mudah karena aplikasi aplikasi kari dan parsial bekerja dengan mudah. Dengan kata lain, apa yang menyenangkan lebih penting daripada apa yang mungkin di sini.
CA McCann
Setiap kali saya melihat contoh kode pemrograman fungsional, saya lebih yakin ini adalah lelucon yang rumit dan tidak ada.
Kieveli
1
@ Kieveli sangat disayangkan kamu merasa seperti itu. Ada banyak tutorial bagus di luar sana yang akan membuat Anda bangun dan berjalan dengan pemahaman dasar yang baik.