Saya baru belajar tentang kari, dan sementara saya pikir saya mengerti konsepnya, saya tidak melihat keuntungan besar dalam menggunakannya.
Sebagai contoh sepele saya menggunakan fungsi yang menambahkan dua nilai (ditulis dalam ML). Versi tanpa kari akan
fun add(x, y) = x + y
dan akan disebut sebagai
add(3, 5)
sedangkan versi kari adalah
fun add x y = x + y
(* short for val add = fn x => fn y=> x + y *)
dan akan disebut sebagai
add 3 5
Sepertinya saya hanya gula sintaksis yang menghilangkan satu set tanda kurung dari mendefinisikan dan memanggil fungsi. Saya telah melihat currying terdaftar sebagai salah satu fitur penting dari bahasa fungsional, dan saya agak kurang puas dengan itu saat ini. Konsep menciptakan rangkaian fungsi yang menggunakan masing-masing parameter tunggal, alih-alih fungsi yang menggunakan tupel tampaknya agak rumit untuk digunakan untuk perubahan sintaksis yang sederhana.
Apakah sintaks sedikit lebih sederhana satu-satunya motivasi untuk kari, atau apakah saya kehilangan beberapa keuntungan lain yang tidak jelas dalam contoh saya yang sangat sederhana? Apakah hanya kari gula sintaksis?
sumber
Jawaban:
Dengan fungsi kari, Anda dapat lebih mudah menggunakan kembali fungsi yang lebih abstrak, karena Anda dapat mengkhususkan diri. Katakanlah Anda memiliki fungsi penambahan
dan bahwa Anda ingin menambahkan 2 ke setiap anggota daftar. Di Haskell Anda akan melakukan ini:
Di sini sintaksinya lebih ringan daripada jika Anda harus membuat suatu fungsi
add2
atau jika Anda harus membuat fungsi lambda anonim:
Ini juga memungkinkan Anda untuk abstrak dari berbagai implementasi. Katakanlah Anda memiliki dua fungsi pencarian. Satu dari daftar pasangan kunci / nilai dan kunci ke nilai dan lainnya dari peta dari kunci ke nilai dan kunci ke nilai, seperti ini:
Kemudian Anda bisa membuat fungsi yang menerima fungsi pencarian dari Key to Value. Anda bisa memberikan fungsi pencarian di atas, yang diterapkan sebagian dengan daftar atau peta, secara berurutan:
Kesimpulannya: currying baik, karena memungkinkan Anda untuk mengkhususkan / menerapkan sebagian fungsi menggunakan sintaks yang ringan dan kemudian meneruskan fungsi yang diterapkan sebagian ini ke fungsi urutan yang lebih tinggi seperti
map
ataufilter
. Fungsi orde tinggi (yang mengambil fungsi sebagai parameter atau menghasilkannya sebagai hasilnya) adalah roti dan mentega dari pemrograman fungsional, dan fungsi currying dan sebagian diterapkan memungkinkan fungsi orde tinggi untuk digunakan jauh lebih efektif dan ringkas.sumber
Jawaban praktisnya adalah membuat kari membuat fungsi anonim menjadi lebih mudah. Bahkan dengan sintaks lambda minimal, itu sesuatu yang menang; membandingkan:
Jika Anda memiliki sintaks lambda jelek, itu lebih buruk lagi. (Saya melihat Anda, JavaScript, Skema, dan Python.)
Ini menjadi semakin berguna saat Anda menggunakan lebih banyak fungsi tingkat tinggi. Sementara saya menggunakan lebih banyak fungsi tingkat tinggi di Haskell daripada di bahasa lain, saya menemukan saya sebenarnya menggunakan sintaks lambda lebih sedikit karena sekitar dua pertiga waktu, lambda hanya akan menjadi fungsi yang diterapkan sebagian. (Dan sebagian besar waktu saya mengekstraknya menjadi fungsi bernama.)
Lebih mendasar lagi, tidak selalu jelas versi fungsi mana yang "kanonik". Sebagai contoh, ambil
map
. Jenismap
dapat ditulis dalam dua cara:Yang mana yang "benar"? Sebenarnya sulit dikatakan. Dalam praktiknya, sebagian besar bahasa menggunakan yang pertama - peta mengambil fungsi dan daftar dan mengembalikan daftar. Namun, pada dasarnya, apa yang sebenarnya dilakukan peta adalah memetakan fungsi normal untuk membuat daftar fungsi - dibutuhkan fungsi dan mengembalikan fungsi. Jika peta digembok, Anda tidak harus menjawab pertanyaan ini: peta keduanya , dengan cara yang sangat elegan.
Ini menjadi sangat penting setelah Anda menyamaratakan
map
jenis selain daftar.Juga, kari sebenarnya tidak terlalu rumit. Ini sebenarnya sedikit penyederhanaan atas model yang digunakan kebanyakan bahasa: Anda tidak memerlukan gagasan fungsi dari beberapa argumen yang dimasukkan ke dalam bahasa Anda. Ini juga mencerminkan kalkulus lambda yang mendasari lebih dekat.
Tentu saja, bahasa gaya ML tidak memiliki gagasan beberapa argumen dalam bentuk kari atau tidak. The
f(a, b, c)
sintaks benar-benar sesuai dengan lewat di tuple(a, b, c)
dalamf
, sehinggaf
masih hanya mengambil argumen. Ini sebenarnya perbedaan yang sangat berguna yang saya harapkan dimiliki oleh bahasa lain karena sangat alami untuk menulis sesuatu seperti:Anda tidak dapat dengan mudah melakukan ini dengan bahasa yang memiliki gagasan beberapa argumen yang dipasangkan langsung!
sumber
Currying mungkin berguna jika Anda memiliki fungsi yang Anda lewati sebagai objek kelas satu, dan Anda tidak menerima semua parameter yang diperlukan untuk mengevaluasinya di satu tempat dalam kode. Anda cukup menerapkan satu atau lebih parameter ketika Anda mendapatkannya dan meneruskan hasilnya ke bagian kode lain yang memiliki lebih banyak parameter dan selesai mengevaluasinya di sana.
Kode untuk mencapai ini akan lebih sederhana daripada jika Anda harus mendapatkan semua parameter bersama terlebih dahulu.
Selain itu, ada kemungkinan lebih banyak penggunaan kembali kode, karena fungsi yang menggunakan satu parameter (fungsi kari lainnya) tidak harus cocok dengan semua parameter secara spesifik.
sumber
Motivasi utama (setidaknya pada awalnya) untuk kari tidak praktis tetapi teoritis. Secara khusus, currying memungkinkan Anda mendapatkan fungsi multi-argumen secara efektif tanpa benar-benar mendefinisikan semantik untuknya atau mendefinisikan semantik untuk produk. Ini mengarah ke bahasa yang lebih sederhana dengan ekspresi yang sama banyaknya dengan bahasa lain yang lebih rumit, dan sangat diinginkan.
sumber
(Saya akan memberikan contoh di Haskell.)
Saat menggunakan bahasa fungsional, sangat mudah bagi Anda untuk menerapkan sebagian fungsi. Seperti dalam Haskell
(== x)
adalah fungsi yang mengembalikanTrue
jika argumennya sama dengan istilah yang diberikanx
:tanpa currying, kita akan memiliki kode yang kurang mudah dibaca:
Ini terkait dengan pemrograman Tacit (lihat juga gaya Pointfree di Haskell wiki). Gaya ini tidak berfokus pada nilai-nilai yang diwakili oleh variabel, tetapi pada penyusunan fungsi dan bagaimana informasi mengalir melalui rantai fungsi. Kami dapat mengonversi contoh kami menjadi formulir yang tidak menggunakan variabel sama sekali:
Di sini kita melihat
==
sebagai fungsi daria
kea -> Bool
danany
sebagai fungsi daria -> Bool
ke[a] -> Bool
. Dengan hanya membuat mereka, kita mendapatkan hasilnya. Ini semua berkat kari.Kebalikannya, un-currying, juga berguna dalam beberapa situasi. Sebagai contoh, katakanlah kita ingin membagi daftar menjadi dua bagian - elemen yang lebih kecil dari 10 dan sisanya, dan kemudian menggabungkan kedua daftar tersebut. Pemisahan daftar dilakukan oleh (di sini kami juga menggunakan kari ). Hasilnya adalah tipe . Alih-alih mengekstraksi hasilnya menjadi bagian pertama dan kedua dan menggabungkannya menggunakan , kita bisa melakukan ini secara langsung dengan tidak memburunya sebagai
partition
(< 10)
<
([Int],[Int])
++
++
Memang,
(uncurry (++) . partition (< 10)) [4,12,11,1]
mengevaluasi untuk[4,1,12,11]
.Ada juga keunggulan teoretis yang penting:
(a, b) -> c
kea -> (b -> c)
berarti bahwa hasil dari fungsi yang terakhir adalah tipeb -> c
. Dengan kata lain, hasilnya adalah fungsi.sumber
mem x lst = any (\y -> y == x) lst
? (Dengan backslash).Currying bukan hanya gula sintaksis!
Pertimbangkan tanda tangan jenis
add1
(uncurried) danadd2
(kari):(Dalam kedua kasus, tanda kurung dalam tanda tangan jenis adalah opsional, tapi saya sudah memasukkannya untuk kejelasan.)
add1
adalah fungsi yang mengambil 2-tupel dariint
danint
dan mengembalikan sebuahint
.add2
adalah fungsi yang mengambilint
dan mengembalikan fungsi lain yang pada gilirannya mengambilint
dan mengembalikanint
.Perbedaan mendasar antara keduanya menjadi lebih terlihat ketika kita menentukan aplikasi fungsi secara eksplisit. Mari kita mendefinisikan fungsi (bukan curried) yang menerapkan argumen pertamanya ke argumen keduanya:
Sekarang kita bisa melihat perbedaan antara
add1
danadd2
lebih jelas.add1
dipanggil dengan 2-tuple:tetapi
add2
dipanggil denganint
dan kemudian nilai kembalinya disebut dengan yang lainint
:EDIT: Manfaat penting dari currying adalah bahwa Anda mendapatkan aplikasi parsial gratis. Katakanlah Anda ingin fungsi tipe
int -> int
(katakanlah, untukmap
itu di atas daftar) yang menambahkan 5 ke parameternya. Anda bisa menulisaddFiveToParam x = x+5
, atau Anda bisa melakukan yang setara dengan lambda inline, tetapi Anda juga bisa jauh lebih mudah (terutama dalam kasus yang kurang sepele daripada yang ini) menulisadd2 5
!sumber
Currying hanya gula sintaksis, tapi Anda sedikit salah paham apa yang dilakukan gula, saya pikir. Mengambil contoh Anda,
sebenarnya gula sintaksis untuk
Yaitu, (tambah x) mengembalikan fungsi yang mengambil argumen y, dan menambahkan x ke y.
Itu adalah fungsi yang mengambil tuple dan menambahkan elemen-elemennya. Kedua fungsi tersebut sebenarnya cukup berbeda; mereka mengambil argumen yang berbeda.
Jika Anda ingin menambahkan 2 ke semua nomor dalam daftar:
Hasilnya adalah
[3,4,5]
.Jika Anda ingin menjumlahkan setiap tuple dalam daftar, di sisi lain, fungsi addTuple sangat cocok.
Hasilnya adalah
[12,13,14]
.Fungsi kari sangat bagus di mana sebagian aplikasi berguna - misalnya peta, lipatan, aplikasi, filter. Pertimbangkan fungsi ini, yang mengembalikan angka positif terbesar dalam daftar yang disediakan, atau 0 jika tidak ada angka positif:
sumber
Hal lain yang belum saya lihat disebutkan adalah bahwa currying memungkinkan (terbatas) abstraksi atas arity.
Pertimbangkan fungsi-fungsi ini yang merupakan bagian dari perpustakaan Haskell
Dalam setiap kasus, variabel tipe
c
dapat berupa tipe fungsi sehingga fungsi-fungsi ini bekerja pada beberapa awalan dari daftar parameter argumen mereka. Tanpa penjelajahan, Anda memerlukan fitur bahasa khusus untuk abstrak di atas arity fungsi atau memiliki banyak versi berbeda dari fungsi-fungsi ini khusus untuk arities yang berbeda.sumber
Pemahaman saya yang terbatas adalah:
1) Aplikasi Fungsi Parsial
Aplikasi Fungsi Parsial adalah proses mengembalikan fungsi yang membutuhkan jumlah argumen yang lebih sedikit. Jika Anda memberikan 2 dari 3 argumen, itu akan mengembalikan fungsi yang membutuhkan 3-2 = 1 argumen. Jika Anda memberikan 1 dari 3 argumen, itu akan mengembalikan fungsi yang membutuhkan 3-1 = 2 argumen. Jika Anda mau, Anda bahkan dapat menerapkan sebagian dari 3 argumen dan itu akan mengembalikan fungsi yang tidak menggunakan argumen.
Jadi diberikan fungsi sebagai berikut:
Saat mengikat 1 ke x dan menerapkannya sebagian ke fungsi di atas
f(x,y,z)
Anda akan mendapatkan:Dimana:
f'(y,z) = 1 + y + z;
Sekarang jika Anda mengikat y ke 2 dan z ke 3, dan menerapkan sebagian
f'(y,z)
Anda akan mendapatkan:Dimana
f''() = 1 + 2 + 3
:;Sekarang kapan saja, Anda dapat memilih untuk mengevaluasi
f
,f'
atauf''
. Jadi saya bisa melakukan:atau
2) Kari
Currying di sisi lain adalah proses pemisahan fungsi menjadi rantai bertingkat dari satu fungsi argumen. Anda tidak pernah dapat memberikan lebih dari 1 argumen, itu satu atau nol.
Jadi diberikan fungsi yang sama:
Jika Anda menjilatnya, Anda akan mendapatkan rantai 3 fungsi:
Dimana:
Sekarang jika Anda menelepon
f'(x)
denganx = 1
:Anda dikembalikan fungsi baru:
Jika Anda menelepon
g(y)
dengany = 2
:Anda dikembalikan fungsi baru:
Akhirnya jika Anda menelepon
h(z)
denganz = 3
:Anda dikembalikan
6
.3) Penutupan
Akhirnya, Penutupan adalah proses menangkap fungsi dan data bersama sebagai satu unit. Penutupan fungsi dapat mengambil 0 hingga jumlah argumen yang tak terbatas, tetapi juga mengetahui data yang tidak diteruskan ke argumen itu.
Sekali lagi, diberikan fungsi yang sama:
Anda malah bisa menulis penutupan:
Dimana:
f'
ditutupx
. Artinyaf'
bisa membaca nilai x yang ada di dalamnyaf
.Jadi, jika Anda menelepon
f
denganx = 1
:Anda akan mendapatkan penutupan:
Sekarang jika Anda menelepon
closureOfF
dengany = 2
danz = 3
:Yang akan kembali
6
Kesimpulan
Currying, aplikasi parsial dan penutupan semuanya agak mirip karena mereka menguraikan fungsi menjadi lebih banyak bagian.
Currying menguraikan fungsi argumen berganda menjadi fungsi bersarang dari argumen tunggal yang mengembalikan fungsi argumen tunggal. Tidak ada gunanya menjelajah fungsi satu atau kurang argumen, karena itu tidak masuk akal.
Aplikasi parsial menguraikan fungsi argumen berganda menjadi fungsi argumen yang lebih rendah yang argumennya yang hilang sekarang diganti dengan nilai yang disediakan.
Penutupan menguraikan fungsi menjadi fungsi dan dataset di mana variabel di dalam fungsi yang tidak diteruskan dapat melihat ke dalam dataset untuk menemukan nilai yang akan diikat ketika diminta untuk mengevaluasi.
Apa yang membingungkan tentang semua ini adalah bahwa mereka dapat jenis masing-masing digunakan untuk mengimplementasikan subset dari yang lain. Jadi pada intinya, mereka semua sedikit detail implementasi. Mereka semua memberikan nilai yang sama di mana Anda tidak perlu mengumpulkan semua nilai di muka dan bahwa Anda dapat menggunakan kembali bagian dari fungsi, karena Anda telah menguraikannya menjadi unit-unit yang bijaksana.
Penyingkapan
Saya sama sekali bukan ahli topik, saya baru saja mulai belajar tentang ini, dan jadi saya memberikan pemahaman saya saat ini, tetapi bisa saja ada kesalahan yang saya undang untuk Anda tunjukkan, dan saya akan mengoreksi sebagai / jika Saya menemukan apa pun.
sumber
Currying (aplikasi sebagian) memungkinkan Anda membuat fungsi baru dari fungsi yang ada dengan memperbaiki beberapa parameter. Ini adalah kasus khusus dari penutupan leksikal di mana fungsi anonim hanyalah pembungkus sepele yang meneruskan beberapa argumen yang diambil ke fungsi lain. Kita juga dapat melakukan ini dengan menggunakan sintaksis umum untuk membuat penutupan leksikal, tetapi aplikasi parsial menyediakan gula sintaksis yang disederhanakan.
Inilah sebabnya mengapa programmer Lisp, ketika bekerja dalam gaya fungsional, kadang-kadang menggunakan perpustakaan untuk aplikasi parsial .
Alih-alih
(lambda (x) (+ 3 x))
, yang memberi kita fungsi yang menambahkan 3 ke argumennya, Anda dapat menulis sesuatu seperti(op + 3)
, dan untuk menambahkan 3 ke setiap elemen daftar beberapa akan(mapcar (op + 3) some-list)
lebih daripada(mapcar (lambda (x) (+ 3 x)) some-list)
.op
Makro ini akan membuat Anda suatu fungsi yang mengambil beberapa argumenx y z ...
dan memanggil(+ a x y z ...)
.Dalam banyak bahasa murni fungsional, aplikasi parsial tertanam ke dalam sintaksis sehingga tidak ada
op
operator. Untuk memicu sebagian aplikasi, Anda cukup memanggil fungsi dengan argumen lebih sedikit dari yang dibutuhkan. Alih-alih menghasilkan"insufficient number of arguments"
kesalahan, hasilnya adalah fungsi dari argumen yang tersisa.sumber
a -> b -> c
tidak memiliki parameter s (jamak), ia hanya memiliki satu parameterc
,. Saat dipanggil, ia mengembalikan fungsi tipea -> b
.Untuk fungsinya
Itu adalah bentuk
f': 'a * 'b -> 'c
Untuk mengevaluasi satu akan dilakukan
Untuk fungsi kari
Untuk mengevaluasi satu akan dilakukan
Di mana itu adalah perhitungan parsial, khususnya (3 + y), yang kemudian dapat diselesaikan dengan perhitungan
tambahkan dalam kasus kedua adalah formulir
f: 'a -> 'b -> 'c
Apa yang kari lakukan di sini adalah mengubah fungsi yang mengambil dua perjanjian menjadi satu yang hanya membutuhkan satu mengembalikan hasil. Evaluasi parsial
Katakan
x
pada RHS bukan hanya int biasa, tetapi juga perhitungan kompleks yang membutuhkan waktu beberapa saat untuk menyelesaikan, untuk tambahan, demi, dua detik.Jadi fungsinya sekarang terlihat seperti
Jenis
add : int * int -> int
Sekarang kita ingin menghitung fungsi ini untuk sejumlah angka, mari kita petakan
Untuk di atas hasil
twoSecondsComputation
dievaluasi setiap saat. Ini berarti dibutuhkan 6 detik untuk perhitungan ini.Menggunakan kombinasi pementasan dan kari seseorang dapat menghindari ini.
Dari bentuk kari
add : int -> int -> int
Sekarang orang bisa melakukannya,
The
twoSecondsComputation
satunya yang perlu dievaluasi sekali. Untuk meningkatkan skala, ganti dua detik dengan 15 menit, atau jam berapa pun, lalu buat peta dengan 100 angka.Ringkasan : Kari sangat bagus bila digunakan dengan metode lain untuk fungsi level yang lebih tinggi sebagai alat evaluasi parsial. Tujuannya tidak dapat benar-benar ditunjukkan dengan sendirinya.
sumber
Currying memungkinkan komposisi fungsi yang fleksibel.
Saya membuat fungsi "kari". Dalam konteks ini, saya tidak peduli jenis logger apa yang saya dapatkan atau dari mana asalnya. Saya tidak peduli apa tindakannya atau dari mana asalnya. Yang saya pedulikan hanyalah memproses input saya.
Variabel pembangun adalah fungsi yang mengembalikan fungsi yang mengembalikan fungsi yang mengambil input saya yang melakukan pekerjaan saya. Ini adalah contoh sederhana yang bermanfaat dan bukan objek yang terlihat.
sumber
Kari adalah keuntungan ketika Anda tidak memiliki semua argumen untuk suatu fungsi. Jika Anda sepenuhnya mengevaluasi fungsi, maka tidak ada perbedaan yang signifikan.
Currying memungkinkan Anda menghindari menyebutkan parameter yang belum dibutuhkan. Itu lebih ringkas, dan tidak perlu menemukan nama parameter yang tidak bertabrakan dengan variabel lain dalam lingkup (yang merupakan manfaat favorit saya).
Misalnya, ketika menggunakan fungsi yang mengambil fungsi sebagai argumen, Anda akan sering menemukan diri Anda dalam situasi di mana Anda membutuhkan fungsi seperti "tambah 3 ke input" atau "bandingkan input ke variabel v". Dengan currying, fungsi-fungsi ini mudah ditulis:
add 3
dan(== v)
. Tanpa kari, Anda harus menggunakan ekspresi lambda:x => add 3 x
danx => x == v
. Ekspresi lambda dua kali lebih panjang, dan memiliki sejumlah kecil pekerjaan yang sibuk terkait dengan memilih nama selainx
jika sudah adax
dalam ruang lingkup.Manfaat sampingan dari bahasa berdasarkan currying adalah bahwa, ketika menulis kode generik untuk fungsi, Anda tidak berakhir dengan ratusan varian berdasarkan jumlah parameter. Misalnya, dalam C #, metode 'kari' akan membutuhkan varian untuk Fungsi <R>, Fungsi <A, R>, Fungsi <A1, A2, R>, Fungsi <A1, A2, A3, R>, dan sebagainya selama-lamanya. Dalam Haskell, ekuivalen dari Func <A1, A2, R> lebih seperti Func <Tuple <A1, A2>, R> atau Func <A1, Func <A2, R >> (dan Func <R> lebih seperti Func <Unit, R>), jadi semua varian sesuai dengan kasus Func <A, R> tunggal.
sumber
Alasan utama yang dapat saya pikirkan (dan saya bukan ahli dalam hal ini dengan segala cara) mulai menunjukkan manfaatnya ketika fungsi-fungsi bergerak dari sepele ke non-sepele. Dalam semua kasus sepele dengan sebagian besar konsep seperti ini Anda tidak akan menemukan manfaat nyata. Namun, sebagian besar bahasa fungsional banyak menggunakan stack dalam operasi pemrosesan. Pertimbangkan PostScript atau Lisp sebagai contohnya. Dengan memanfaatkan currying, fungsi dapat ditumpuk dengan lebih efektif dan manfaat ini menjadi semakin jelas seiring operasi yang tumbuh semakin tidak sepele. Dengan cara yang dijaga, perintah dan argumen dapat dilemparkan pada tumpukan secara berurutan dan muncul sesuai kebutuhan sehingga dijalankan dalam urutan yang tepat.
sumber
Kari tergantung sangat penting (bahkan pasti) pada kemampuan untuk mengembalikan fungsi.
Pertimbangkan kode semu ini (dibuat-buat).
var f = (m, x, b) => ... kembalikan sesuatu ...
Mari kita menetapkan bahwa memanggil f dengan kurang dari tiga argumen mengembalikan fungsi.
var g = f (0, 1); // ini mengembalikan fungsi yang terikat ke 0 dan 1 (m dan x) yang menerima satu argumen lagi (b).
var y = g (42); // aktifkan g dengan argumen ketiga yang hilang, menggunakan 0 dan 1 untuk m dan x
Bahwa Anda dapat menerapkan sebagian argumen dan mendapatkan kembali fungsi yang dapat digunakan kembali (terikat pada argumen yang Anda berikan) cukup berguna (dan KERING).
sumber