Apa keuntungan dari kari?

154

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?

Ilmuwan gila
sumber
54
Currying sendirian pada dasarnya tidak berguna, tetapi memiliki semua fungsi yang digulung secara default membuat banyak fitur lainnya jauh lebih baik untuk digunakan. Sulit untuk menghargai ini sampai Anda benar-benar menggunakan bahasa fungsional untuk sementara waktu.
CA McCann
4
Sesuatu yang disebutkan secara sepintas oleh delnan dalam komentar pada jawaban JoelEtherton, tetapi yang saya pikir akan saya sebutkan secara eksplisit, adalah bahwa (setidaknya di Haskell) Anda dapat menerapkan sebagian dengan tidak hanya fungsi tetapi juga mengetik konstruktor - ini bisa sangat berguna; ini mungkin sesuatu untuk dipikirkan.
paul
Semua telah memberikan contoh Haskell. Orang mungkin bertanya-tanya kari hanya berguna di Haskell.
Manoj R
@ ManojR Semua belum memberikan contoh di Haskell.
phwd
1
Pertanyaan itu menghasilkan diskusi yang cukup menarik tentang Reddit .
yannis

Jawaban:

126

Dengan fungsi kari, Anda dapat lebih mudah menggunakan kembali fungsi yang lebih abstrak, karena Anda dapat mengkhususkan diri. Katakanlah Anda memiliki fungsi penambahan

add x y = x + y

dan bahwa Anda ingin menambahkan 2 ke setiap anggota daftar. Di Haskell Anda akan melakukan ini:

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

Di sini sintaksinya lebih ringan daripada jika Anda harus membuat suatu fungsi add2

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

atau jika Anda harus membuat fungsi lambda anonim:

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

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:

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

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:

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

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 mapatau filter. 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.

Boris
sumber
31
Perlu dicatat bahwa, karena ini, urutan argumen yang digunakan untuk fungsi di Haskell sering didasarkan pada seberapa besar kemungkinan aplikasi parsial, yang pada gilirannya membuat manfaat yang dijelaskan di atas berlaku (ha, ha) dalam lebih banyak situasi. Menjelajahi secara default dengan demikian pada akhirnya menjadi lebih bermanfaat daripada yang terlihat dari contoh spesifik seperti yang ada di sini.
CA McCann
wat. "Satu dari daftar pasangan kunci / nilai dan kunci ke nilai dan lainnya dari peta dari kunci ke nilai dan kunci ke nilai"
Mateen Ulhaq
@MateenUlhaq Ini adalah kelanjutan dari kalimat sebelumnya, di mana saya kira kita ingin mendapatkan nilai berdasarkan kunci, dan kami memiliki dua cara untuk melakukan itu. Kalimat itu menyebutkan dua cara. Pada cara pertama, Anda diberi daftar pasangan kunci / nilai dan kunci yang ingin kami temukan nilainya, dan dengan cara lain kami diberikan peta yang tepat, dan lagi kunci. Mungkin membantu untuk melihat kode segera setelah kalimat.
Boris
53

Jawaban praktisnya adalah membuat kari membuat fungsi anonim menjadi lebih mudah. Bahkan dengan sintaks lambda minimal, itu sesuatu yang menang; membandingkan:

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

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. Jenis mapdapat ditulis dalam dua cara:

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

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 mapjenis 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)dalam f, sehingga fmasih hanya mengambil argumen. Ini sebenarnya perbedaan yang sangat berguna yang saya harapkan dimiliki oleh bahasa lain karena sangat alami untuk menulis sesuatu seperti:

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

Anda tidak dapat dengan mudah melakukan ini dengan bahasa yang memiliki gagasan beberapa argumen yang dipasangkan langsung!

Tikhon Jelvis
sumber
1
"Bahasa gaya-ML tidak memiliki gagasan beberapa argumen dalam bentuk kari atau dalam bentuk yang tidak terburu-buru": dalam hal ini, apakah gaya Haskell ML?
Giorgio
1
@Iorgio: Ya.
Tikhon Jelvis
1
Menarik. Saya tahu beberapa Haskell dan saya sedang belajar SML sekarang, jadi menarik untuk melihat perbedaan dan persamaan antara kedua bahasa.
Giorgio
Jawaban yang bagus, dan jika Anda masih tidak yakin, pikirkan saja pipeline Unix yang mirip dengan lambda stream
Sridhar Sarnobat
Jawaban "praktis" tidak terlalu relevan karena verbositas biasanya dihindari dengan aplikasi parsial , bukan currying. Dan saya berpendapat di sini sintaks abstraksi lambda (terlepas dari tipe deklarasi) lebih buruk dari itu (setidaknya) dalam Skema karena membutuhkan lebih banyak aturan sintaksis khusus untuk menguraikannya dengan benar, yang membengkak-bumbar spesifikasi bahasa tanpa keuntungan apa pun tentang sifat semantik.
FrankHB
24

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.

psr
sumber
14

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.

Alex R
sumber
2
Walaupun motivasi di sini bersifat teoretis, saya pikir kesederhanaan hampir selalu merupakan keuntungan praktis juga. Tidak khawatir tentang fungsi multi-argumen membuat hidup saya lebih mudah ketika saya memprogram, seperti halnya jika saya bekerja dengan semantik.
Tikhon Jelvis
2
@TikhonJelvis Ketika Anda pemrograman, currying memberi Anda hal-hal lain yang perlu dikhawatirkan, seperti kompiler tidak menangkap fakta bahwa Anda melewati terlalu sedikit argumen ke suatu fungsi, atau bahkan mendapatkan pesan kesalahan yang buruk dalam kasus itu; ketika Anda tidak menggunakan kari, kesalahannya jauh lebih jelas.
Alex R
Saya tidak pernah memiliki masalah seperti itu: GHC, paling tidak, sangat bagus dalam hal itu. Kompiler selalu menangkap masalah semacam itu, dan memiliki pesan kesalahan yang baik untuk kesalahan ini juga.
Tikhon Jelvis
1
Saya tidak setuju bahwa pesan kesalahan memenuhi syarat baik. Dapat digunakan, ya, tetapi mereka belum bagus. Itu juga hanya menangkap masalah semacam itu jika menghasilkan kesalahan tipe, yaitu jika Anda kemudian mencoba menggunakan hasilnya sebagai sesuatu selain fungsi (atau Anda telah mengetik beranotasi, tetapi mengandalkan bahwa untuk kesalahan yang dapat dibaca memiliki masalah sendiri. ); lokasi kesalahan yang dilaporkan dipisahkan dari lokasi sebenarnya.
Alex R
14

(Saya akan memberikan contoh di Haskell.)

  1. Saat menggunakan bahasa fungsional, sangat mudah bagi Anda untuk menerapkan sebagian fungsi. Seperti dalam Haskell (== x)adalah fungsi yang mengembalikan Truejika argumennya sama dengan istilah yang diberikan x:

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

    tanpa currying, kita akan memiliki kode yang kurang mudah dibaca:

    mem x lst = any (\y -> y == x) lst
    
  2. 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:

    mem = any . (==)
    

    Di sini kita melihat ==sebagai fungsi dari ake a -> Booldan anysebagai fungsi dari a -> Boolke [a] -> Bool. Dengan hanya membuat mereka, kita mendapatkan hasilnya. Ini semua berkat kari.

  3. 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 sebagaipartition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

Memang, (uncurry (++) . partition (< 10)) [4,12,11,1]mengevaluasi untuk [4,1,12,11].

Ada juga keunggulan teoretis yang penting:

  1. Kari sangat penting untuk bahasa yang tidak memiliki tipe data dan hanya memiliki fungsi, seperti kalkulus lambda . Meskipun bahasa ini tidak berguna untuk penggunaan praktis, mereka sangat penting dari sudut pandang teoritis.
  2. Ini terhubung dengan properti esensial dari bahasa fungsional - fungsi adalah objek kelas satu. Seperti yang telah kita lihat, konversi dari (a, b) -> cke a -> (b -> c)berarti bahwa hasil dari fungsi yang terakhir adalah tipe b -> c. Dengan kata lain, hasilnya adalah fungsi.
  3. (Un) currying terkait erat dengan kategori tertutup kartesius , yang merupakan cara kategoris untuk melihat kalkulus lambda yang diketik.
Petr Pudlák
sumber
Untuk bit "kode yang jauh lebih mudah dibaca", bukankah begitu mem x lst = any (\y -> y == x) lst? (Dengan backslash).
stusmith
Ya, terima kasih telah menunjukkan itu, saya akan memperbaikinya.
Petr Pudlák
9

Currying bukan hanya gula sintaksis!

Pertimbangkan tanda tangan jenis add1(uncurried) dan add2(kari):

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

(Dalam kedua kasus, tanda kurung dalam tanda tangan jenis adalah opsional, tapi saya sudah memasukkannya untuk kejelasan.)

add1adalah fungsi yang mengambil 2-tupel dari intdan intdan mengembalikan sebuah int. add2adalah fungsi yang mengambil intdan mengembalikan fungsi lain yang pada gilirannya mengambil intdan mengembalikan int.

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:

apply(f, b) = f b

Sekarang kita bisa melihat perbedaan antara add1dan add2lebih jelas. add1dipanggil dengan 2-tuple:

apply(add1, (3, 5))

tetapi add2dipanggil dengan int dan kemudian nilai kembalinya disebut dengan yang lainint :

apply(apply(add2, 3), 5)

EDIT: Manfaat penting dari currying adalah bahwa Anda mendapatkan aplikasi parsial gratis. Katakanlah Anda ingin fungsi tipe int -> int(katakanlah, untuk mapitu di atas daftar) yang menambahkan 5 ke parameternya. Anda bisa menulis addFiveToParam 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) menulis add2 5!

Api Ptharien
sumber
3
Saya mengerti bahwa ada perbedaan besar di belakang layar untuk contoh saya, tetapi hasilnya tampaknya menjadi perubahan sintaksis sederhana.
Mad Scientist
5
Kari bukan konsep yang sangat mendalam. Ini adalah tentang menyederhanakan model yang mendasarinya (lihat Lambda Calculus) atau dalam bahasa yang memiliki tuple, ini sebenarnya tentang kenyamanan sintaksis aplikasi parsial. Jangan meremehkan pentingnya kenyamanan sintaksis.
Peaker
9

Currying hanya gula sintaksis, tapi Anda sedikit salah paham apa yang dilakukan gula, saya pikir. Mengambil contoh Anda,

fun add x y = x + y

sebenarnya gula sintaksis untuk

fun add x = fn y => x + y

Yaitu, (tambah x) mengembalikan fungsi yang mengambil argumen y, dan menambahkan x ke y.

fun addTuple (x, y) = x + 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:

(* 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]

Hasilnya adalah [3,4,5].

Jika Anda ingin menjumlahkan setiap tuple dalam daftar, di sisi lain, fungsi addTuple sangat cocok.

(* 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)]

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:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 
Agak
sumber
1
Saya mengerti bahwa fungsi kari memiliki tipe tanda tangan yang berbeda, dan sebenarnya itu adalah fungsi yang mengembalikan fungsi lain. Saya melewatkan bagian aplikasi sebagian.
Mad Scientist
9

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

(.) :: (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

Dalam setiap kasus, variabel tipe cdapat 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.

Geoff Reedy
sumber
6

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:

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

Saat mengikat 1 ke x dan menerapkannya sebagian ke fungsi di atas f(x,y,z)Anda akan mendapatkan:

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

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:

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

Dimana f''() = 1 + 2 + 3:;

Sekarang kapan saja, Anda dapat memilih untuk mengevaluasi f, f'atau f''. Jadi saya bisa melakukan:

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

atau

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

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:

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

Jika Anda menjilatnya, Anda akan mendapatkan rantai 3 fungsi:

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

Dimana:

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

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

f'''(z) = z;

Sekarang jika Anda menelepon f'(x)dengan x = 1:

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

Anda dikembalikan fungsi baru:

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

Jika Anda menelepon g(y)dengan y = 2:

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

Anda dikembalikan fungsi baru:

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

Akhirnya jika Anda menelepon h(z)dengan z = 3:

h(3) = 1 + 2 + 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:

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

Anda malah bisa menulis penutupan:

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

Dimana:

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

f'ditutup x. Artinya f'bisa membaca nilai x yang ada di dalamnya f.

Jadi, jika Anda menelepon fdengan x = 1:

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

Anda akan mendapatkan penutupan:

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

Sekarang jika Anda menelepon closureOfFdengan y = 2dan z = 3:

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 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.

Didier A.
sumber
1
Jadi jawabannya adalah: kari tidak menguntungkan?
ceving
1
@ceving Sejauh yang saya tahu, itu benar. Dalam praktiknya aplikasi currying dan parsial akan memberi Anda manfaat yang sama. Pilihan yang akan diterapkan dalam bahasa dibuat karena alasan implementasi, yang satu mungkin lebih mudah diimplementasikan daripada yang lain diberikan bahasa tertentu.
Didier A.
5

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). opMakro ini akan membuat Anda suatu fungsi yang mengambil beberapa argumen x y z ...dan memanggil (+ a x y z ...).

Dalam banyak bahasa murni fungsional, aplikasi parsial tertanam ke dalam sintaksis sehingga tidak ada opoperator. 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.

Kaz
sumber
"Currying ... memungkinkan Anda membuat fungsi baru ... dengan memperbaiki beberapa parameter" - tidak, fungsi tipe a -> b -> ctidak memiliki parameter s (jamak), ia hanya memiliki satu parameter c,. Saat dipanggil, ia mengembalikan fungsi tipe a -> b.
Max Heiber
4

Untuk fungsinya

fun add(x, y) = x + y

Itu adalah bentuk f': 'a * 'b -> 'c

Untuk mengevaluasi satu akan dilakukan

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

Untuk fungsi kari

fun add x y = x + y

Untuk mengevaluasi satu akan dilakukan

add 3
val it = fn : int -> int

Di mana itu adalah perhitungan parsial, khususnya (3 + y), yang kemudian dapat diselesaikan dengan perhitungan

it 5
val it = 8 : int

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

Mengapa orang membutuhkan ini?

Katakan xpada RHS bukan hanya int biasa, tetapi juga perhitungan kompleks yang membutuhkan waktu beberapa saat untuk menyelesaikan, untuk tambahan, demi, dua detik.

x = twoSecondsComputation(z)

Jadi fungsinya sekarang terlihat seperti

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

Jenis add : int * int -> int

Sekarang kita ingin menghitung fungsi ini untuk sejumlah angka, mari kita petakan

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

Untuk di atas hasil twoSecondsComputationdievaluasi setiap saat. Ini berarti dibutuhkan 6 detik untuk perhitungan ini.

Menggunakan kombinasi pementasan dan kari seseorang dapat menghindari ini.

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

Dari bentuk kari add : int -> int -> int

Sekarang orang bisa melakukannya,

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

The twoSecondsComputationsatunya 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.

phwd
sumber
3

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.

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.

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.

mortalapeman
sumber
2

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 3dan (== v). Tanpa kari, Anda harus menggunakan ekspresi lambda: x => add 3 xdan x => x == v. Ekspresi lambda dua kali lebih panjang, dan memiliki sejumlah kecil pekerjaan yang sibuk terkait dengan memilih nama selain xjika sudah ada xdalam 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.

Craig Gidney
sumber
2

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.

Peter Mortensen
sumber
1
Bagaimana tepatnya membutuhkan lebih banyak tumpukan stack yang akan dibuat membuat segalanya lebih efisien?
Mason Wheeler
1
@MasonWheeler: Saya tidak akan tahu, karena saya katakan saya bukan ahli bahasa fungsional atau menjelajah secara khusus. Saya memberi label wiki komunitas ini khusus karena itu.
Joel Etherton
4
@MasonWheeler Anda memiliki titik wrt ungkapan dari jawaban ini, tetapi biarkan saya chip dan mengatakan bahwa jumlah frame stack sebenarnya dibuat sangat tergantung pada implementasi. Misalnya, dalam mesin G tagless tanpa putaran (STG; cara GHC mengimplementasikan Haskell) menunda evaluasi aktual hingga mengakumulasi semua (atau setidaknya sebanyak yang diperlukan) argumen. Saya tidak dapat mengingat apakah ini dilakukan untuk semua fungsi atau hanya untuk konstruktor, tetapi saya pikir itu mungkin untuk sebagian besar fungsi. (Kemudian lagi, konsep "stack frames" tidak benar-benar berlaku untuk STG.)
1

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).

Rick O'Shea
sumber