ประโยชน์ของแกงกะหรี่คืออะไร?


154

ฉันเพิ่งเรียนรู้เกี่ยวกับการแกงและในขณะที่ฉันคิดว่าฉันเข้าใจแนวคิดฉันไม่เห็นประโยชน์ใหญ่หลวงใด ๆ ในการใช้มัน

เป็นตัวอย่างเล็ก ๆ น้อย ๆ ฉันใช้ฟังก์ชั่นที่เพิ่มสองค่า (เขียนเป็น ML) รุ่นที่ไม่มีการปิดบังจะเป็น

fun add(x, y) = x + y

และจะเรียกว่าเป็น

add(3, 5)

ในขณะที่เวอร์ชันแกงกะหรี่คือ

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

และจะเรียกว่าเป็น

add 3 5

มันดูเหมือนว่าฉันจะเป็นเพียงน้ำตาล syntactic ที่ลบวงเล็บหนึ่งชุดจากการกำหนดและเรียกใช้ฟังก์ชัน ฉันเคยเห็นการลงรายการบัญชีเป็นหนึ่งในคุณสมบัติที่สำคัญของภาษาที่ใช้งานได้และฉันรู้สึกสับสนเล็กน้อยในตอนนี้ แนวคิดของการสร้างสายโซ่ของฟังก์ชั่นที่ใช้แต่ละพารามิเตอร์เดียวแทนที่จะเป็นฟังก์ชั่นที่ใช้ tuple ดูเหมือนค่อนข้างซับซ้อนที่จะใช้สำหรับการเปลี่ยนแปลงที่ง่ายของไวยากรณ์

ไวยากรณ์ที่เรียบง่ายกว่าเล็กน้อยเป็นเพียงแรงจูงใจในการปิดตาหรือฉันกำลังพลาดข้อดีอื่น ๆ ที่ไม่ชัดเจนในตัวอย่างง่าย ๆ ของฉันหรือไม่ การแกงน้ำตาลแค่น้ำตาลหรือไม่


54
การปิดกั้นเพียงอย่างเดียวนั้นไม่มีประโยชน์ แต่การมีฟังก์ชั่นทั้งหมดที่ถูกปิดกั้นตามค่าเริ่มต้นทำให้มีคุณสมบัติอื่น ๆ เป็นการยากที่จะเข้าใจสิ่งนี้จนกว่าคุณจะได้ใช้ภาษาที่ใช้งานได้จริงในขณะที่
CA McCann

4
บางสิ่งที่ถูกกล่าวถึงในการส่งโดย delnan ในความคิดเห็นในคำตอบของ JoelEtherton แต่ฉันคิดว่าฉันจะพูดอย่างชัดเจนคือ (อย่างน้อยใน Haskell) คุณสามารถนำไปใช้กับฟังก์ชั่นไม่เพียง แต่บางประเภทเท่านั้น ที่มีประโยชน์; นี่อาจเป็นสิ่งที่คิด
พอล

ทั้งหมดได้รับตัวอย่างของ Haskell หนึ่งอาจสงสัยว่าการแกงเป็นประโยชน์เฉพาะใน Haskell
Manoj R

@ManojR ทั้งหมดไม่ได้รับตัวอย่างใน Haskell
phwd

1
คำถามที่เกิดการอภิปรายที่น่าสนใจอย่างเป็นธรรมในReddit
yannis

คำตอบ:


126

ด้วยฟังก์ชั่น curried คุณจะสามารถนำฟังก์ชั่นที่เป็นนามธรรมมาใช้ได้ง่ายขึ้นเนื่องจากคุณมีความเชี่ยวชาญ สมมติว่าคุณมีฟังก์ชั่นการเพิ่ม

add x y = x + y

และคุณต้องการเพิ่ม 2 ให้กับสมาชิกทุกคนของรายการ ใน Haskell คุณจะทำสิ่งนี้:

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

นี่คือไวยากรณ์เบากว่าถ้าคุณต้องสร้างฟังก์ชั่น add2

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

หรือถ้าคุณต้องสร้างฟังก์ชั่นแลมบ์ดานิรนาม:

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

นอกจากนี้ยังช่วยให้คุณสามารถหลีกเลี่ยงการใช้งานที่แตกต่างกัน สมมติว่าคุณมีฟังก์ชันการค้นหาสองฟังก์ชัน หนึ่งรายการจากคู่ของคีย์ / ค่าและคีย์ไปยังค่าและอีกรายการจากแผนที่จากคีย์ไปยังค่าและคีย์เป็นค่าเช่นนี้:

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

จากนั้นคุณสามารถสร้างฟังก์ชันที่ยอมรับฟังก์ชันการค้นหาจาก Key เป็น Value คุณสามารถส่งผ่านฟังก์ชั่นการค้นหาใด ๆ ข้างต้นได้บางส่วนนำไปใช้กับรายการหรือแผนที่ตามลำดับ:

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

สรุป: currying เป็นสิ่งที่ดีเพราะจะช่วยให้คุณมีความเชี่ยวชาญ / บางส่วนใช้ฟังก์ชั่นการใช้ไวยากรณ์ที่มีน้ำหนักเบาและแล้วผ่านฟังก์ชั่นที่ใช้บางส่วนเหล่านี้ไปรอบ ๆ เพื่อฟังก์ชั่นการสั่งซื้อที่สูงขึ้นเช่นหรือmap filterฟังก์ชั่นการสั่งซื้อที่สูงขึ้น (ซึ่งทำหน้าที่เป็นพารามิเตอร์หรือให้ผลลัพธ์เป็นผลลัพธ์) คือขนมปังและเนยของการเขียนโปรแกรมเชิงฟังก์ชันและฟังก์ชั่นการแกงและการนำไปใช้บางส่วนทำให้ฟังก์ชันการสั่งซื้อที่สูงขึ้น


31
เป็นที่น่าสังเกตว่าด้วยเหตุนี้คำสั่งอาร์กิวเมนต์ที่ใช้สำหรับฟังก์ชันใน Haskell มักจะขึ้นอยู่กับความน่าจะเป็นบางส่วนของแอปพลิเคชันซึ่งจะทำให้ประโยชน์ที่ได้อธิบายไว้ข้างต้นนำไปใช้ (ha, ha) ในสถานการณ์อื่น ๆ การปิดปากตามค่าเริ่มต้นจึงกลายเป็นประโยชน์มากกว่าที่เห็นได้จากตัวอย่างเฉพาะเช่นที่นี่
CA McCann

วัด "หนึ่งจากรายการของคู่คีย์ / ค่าและคีย์เป็นค่าและอีกรายการจากแผนที่จากคีย์ไปยังค่าและคีย์เป็นค่า"
Mateen Ulhaq

@MateenUlhaq มันเป็นความต่อเนื่องของประโยคก่อนหน้านี้ซึ่งฉันคิดว่าเราต้องการได้ค่าตามคีย์และเรามีสองวิธีในการทำเช่นนั้น ประโยคดังกล่าวระบุสองวิธี ในวิธีแรกคุณจะได้รับรายการคู่คีย์ / ค่าและคีย์ที่เราต้องการค้นหาค่าสำหรับและในทางอื่นเราจะได้รับแผนที่ที่เหมาะสมและคีย์อีกครั้ง มันอาจช่วยในการดูรหัสทันทีหลังประโยค
Boris

53

คำตอบที่เป็นประโยชน์คือการทำให้การสร้างฟังก์ชั่นนิรนามนั้นง่ายขึ้นมาก แม้จะมีรูปแบบแลมบ์ดาน้อยที่สุด แต่ก็เป็นชัยชนะที่ได้รับ; เปรียบเทียบ:

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

หากคุณมีรูปแบบแลมบ์ดาที่น่าเกลียดมันยิ่งแย่ลงไปอีก (ฉันกำลังดูคุณ, JavaScript, Scheme และ Python)

สิ่งนี้จะมีประโยชน์มากขึ้นเมื่อคุณใช้ฟังก์ชั่นการสั่งซื้อที่มากขึ้น ในขณะที่ผมใช้เพิ่มเติมฟังก์ชั่นขั้นสูงใน Haskell กว่าในภาษาอื่น ๆ ที่ฉันได้พบจริงผมใช้ไวยากรณ์แลมบ์ดาน้อยเพราะสิ่งที่ต้องการสองในสามของเวลาที่แลมบ์ดาก็จะเป็นฟังก์ชั่นที่ใช้บางส่วน (และอีกหลายครั้งที่ฉันแยกมันเป็นฟังก์ชันที่มีชื่อ)

โดยพื้นฐานแล้วจะไม่ชัดเจนเสมอไปว่าฟังก์ชันรุ่นใดเป็น "มาตรฐาน" mapตัวอย่างเช่นใช้ ประเภทของmapสามารถเขียนได้สองวิธี:

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

อันไหนที่ "ถูกต้อง" อันไหน มันยากที่จะพูด ในทางปฏิบัติภาษาส่วนใหญ่ใช้แผนที่แรกใช้ฟังก์ชั่นและรายการและส่งคืนรายการ อย่างไรก็ตามโดยพื้นฐานแล้วสิ่งที่แผนที่ทำจริงคือการทำแผนที่ฟังก์ชั่นปกติในการแสดงฟังก์ชั่น - ใช้ฟังก์ชั่นและส่งกลับฟังก์ชั่น หากแผนที่ curried คุณไม่จำเป็นต้องตอบคำถามนี้: ทำทั้งสองอย่างในลักษณะที่หรูหรามาก

สิ่งนี้มีความสำคัญอย่างยิ่งเมื่อคุณพูดคุยmapกับประเภทอื่นนอกเหนือจากรายการ

การแกงยังไม่ซับซ้อนอีกด้วย จริงๆแล้วมันเป็นการทำให้เข้าใจง่ายกว่าโมเดลที่ภาษาส่วนใหญ่ใช้: คุณไม่จำเป็นต้องมีแนวคิดเกี่ยวกับฟังก์ชั่นของการถกเถียงกันหลาย ๆ อย่างในภาษาของคุณ สิ่งนี้ยังสะท้อนถึงแคลคูลัสแลมบ์ดาพื้นฐานอย่างใกล้ชิดยิ่งขึ้น

แน่นอนว่าภาษาสไตล์ ML ไม่มีความคิดเห็นเกี่ยวกับข้อโต้แย้งหลายประการในรูปแบบ curried หรือในรูปแบบที่ไม่ได้แปล f(a, b, c)ไวยากรณ์จริงสอดคล้องกับผ่านใน tuple (a, b, c)เข้าไปfเพื่อให้fยังคงใช้เวลาเพียง แต่ในการโต้แย้ง นี่คือความแตกต่างที่มีประโยชน์มากที่ฉันต้องการภาษาอื่น ๆ จะมีเพราะมันเป็นธรรมชาติมากที่จะเขียนสิ่งที่ชอบ:

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

คุณไม่สามารถทำสิ่งนี้ได้อย่างง่ายดายด้วยภาษาที่มีความคิดเรื่องการโต้แย้งหลายอย่างที่ถูกต้อง!


1
"ภาษาสไตล์ ML ไม่มีความคิดเห็นเกี่ยวกับข้อโต้แย้งหลายอย่างในรูปแบบ curried หรือในรูปแบบที่ไม่ได้แปล": ในแง่นี้ Haskell ML-style คืออะไร?
Giorgio

1
@Giorgio: ใช่
Tikhon Jelvis

1
น่าสนใจ ฉันรู้ว่า Haskell และฉันกำลังเรียน SML อยู่ในตอนนี้ดังนั้นมันจึงน่าสนใจที่จะเห็นความแตกต่างและความคล้ายคลึงกันระหว่างสองภาษา
Giorgio

คำตอบที่ยอดเยี่ยมและหากคุณยังไม่มั่นใจลองนึกถึงท่อ Unix ที่คล้ายกับลำธารแลมบ์ดา
Sridhar Sarnobat

คำตอบ "เชิงปฏิบัติ" นั้นไม่เกี่ยวข้องกันมากนักเพราะมักจะหลีกเลี่ยงการใช้คำฟุ่มเฟือยโดยการประยุกต์บางส่วนไม่ใช่การปิดบัง และฉันขอเถียงที่นี่ซินแท็คซ์ของแลมบ์ดา abstraction (แม้จะมีการประกาศประเภท) เป็น uglier กว่า (อย่างน้อย) ใน Scheme เพราะมันต้องการกฎวากยสัมพันธ์พิเศษแบบบิวด์เพิ่มเติมในตัวเพื่อวิเคราะห์มันอย่างถูกต้อง เกี่ยวกับคุณสมบัติความหมาย
FrankHB

24

การแกงอาจมีประโยชน์หากคุณมีฟังก์ชั่นที่คุณใช้เป็นวัตถุชั้นหนึ่งและคุณไม่ได้รับพารามิเตอร์ทั้งหมดที่จำเป็นในการประเมินในที่เดียวในรหัส คุณสามารถใช้พารามิเตอร์อย่างน้อยหนึ่งพารามิเตอร์เมื่อคุณรับพารามิเตอร์เหล่านั้นและส่งผลลัพธ์ไปยังโค้ดอีกส่วนหนึ่งที่มีพารามิเตอร์มากกว่าและเสร็จสิ้นการประเมินที่นั่น

โค้ดที่จะทำให้สิ่งนี้สำเร็จนั้นจะง่ายกว่าถ้าคุณต้องการให้พารามิเตอร์ทั้งหมดมารวมกันก่อน

นอกจากนี้ยังมีความเป็นไปได้ที่จะนำรหัสมาใช้ใหม่เนื่องจากฟังก์ชันที่ใช้พารามิเตอร์เดียว (ฟังก์ชั่น curried อื่น) ไม่จำเป็นต้องจับคู่กับพารามิเตอร์ทั้งหมดโดยเฉพาะ


14

แรงจูงใจหลัก (อย่างน้อยเริ่มแรก) สำหรับการแกงไม่ได้เป็นไปได้ แต่ในทางทฤษฎี โดยเฉพาะอย่างยิ่งการแกงช่วยให้คุณได้รับฟังก์ชั่นหลายอาร์กิวเมนต์อย่างมีประสิทธิภาพโดยไม่ต้องกำหนดความหมายสำหรับพวกเขาหรือกำหนดความหมายสำหรับผลิตภัณฑ์ สิ่งนี้นำไปสู่ภาษาที่เรียบง่ายกว่าและมีความหมายมากกว่าภาษาอื่นภาษาที่ซับซ้อนกว่าและเป็นที่ต้องการ


2
ในขณะที่แรงจูงใจที่นี่เป็นเชิงทฤษฎีฉันคิดว่าความเรียบง่ายมักจะเป็นประโยชน์ในทางปฏิบัติเช่นกัน ไม่ต้องกังวลเกี่ยวกับฟังก์ชั่นหลายอาร์กิวเมนต์ทำให้ชีวิตของฉันง่ายขึ้นเมื่อฉันตั้งโปรแกรมเช่นเดียวกับถ้าฉันทำงานกับซีแมนติกส์
Tikhon Jelvis

2
@TikhonJelvis ขณะที่คุณกำลังเขียนโปรแกรมการ currying ให้สิ่งอื่น ๆ ที่คุณกังวลเช่นคอมไพเลอร์ที่ไม่เข้าใจความจริงที่ว่าคุณส่งอาร์กิวเมนต์น้อยเกินไปไปยังฟังก์ชันหรือได้รับข้อความแสดงข้อผิดพลาดที่ไม่ดี เมื่อคุณไม่ได้ใช้การแกงข้อผิดพลาดนั้นชัดเจนมากขึ้น
Alex R

ฉันไม่เคยมีปัญหาแบบนั้น: อย่างน้อยที่สุด GHC ก็ดีมากในเรื่องนั้น คอมไพเลอร์มักจะจับปัญหาประเภทนั้นและมีข้อความแสดงข้อผิดพลาดที่ดีสำหรับข้อผิดพลาดนี้เช่นกัน
Tikhon Jelvis

1
ฉันไม่สามารถตกลงได้ว่าข้อความแสดงข้อผิดพลาดมีคุณสมบัติที่ดี เป็นประโยชน์ใช่ แต่พวกเขายังไม่ดี นอกจากนี้ยังจับปัญหาประเภทนั้นได้เฉพาะเมื่อมันส่งผลให้เกิดข้อผิดพลาดประเภทเช่นหากคุณพยายามใช้ผลลัพธ์เป็นสิ่งอื่นนอกเหนือจากฟังก์ชั่น (หรือคุณพิมพ์คำอธิบายประกอบไว้แล้ว แต่อาศัยข้อผิดพลาดที่สามารถอ่านได้ ); ตำแหน่งที่รายงานของข้อผิดพลาดถูกหย่าจากตำแหน่งจริง
Alex R

14

(ฉันจะยกตัวอย่างใน Haskell)

  1. เมื่อใช้ภาษาที่ใช้งานได้จะสะดวกมากที่คุณสามารถใช้ฟังก์ชั่นบางส่วนได้ เช่นเดียวกับใน Haskell ของ(== x)ฟังก์ชั่นที่ส่งกลับTrueถ้าอาร์กิวเมนต์เท่ากับระยะที่กำหนดx:

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

    เราจะมีรหัสที่สามารถอ่านได้ค่อนข้างน้อย:

    mem x lst = any (\y -> y == x) lst
    
  2. สิ่งนี้เกี่ยวข้องกับการเขียนโปรแกรม Tacit (โปรดดูสไตล์ Pointfreeบน Haskell wiki) สไตล์นี้ไม่ได้เน้นที่ค่าที่แสดงโดยตัวแปร แต่เป็นการเขียนฟังก์ชั่นและวิธีการที่ข้อมูลไหลผ่านสายโซ่ของฟังก์ชัน เราสามารถแปลงตัวอย่างของเราเป็นรูปแบบที่ไม่ใช้ตัวแปรเลย:

    mem = any . (==)
    

    ที่นี่เราดู==เป็นฟังก์ชั่นจากaไปa -> Boolและanyเป็นฟังก์ชั่นจากไปa -> Bool [a] -> Boolเพียงแค่เขียนมันเราจะได้ผลลัพธ์ ทั้งหมดนี้ต้องขอบคุณการแกง

  3. การย้อนกลับการไม่ถอนตัวยังมีประโยชน์ในบางสถานการณ์ ตัวอย่างเช่นสมมติว่าเราต้องการแยกรายการออกเป็นสองส่วน - องค์ประกอบที่มีขนาดเล็กกว่า 10 และส่วนที่เหลือจากนั้นเชื่อมโยงรายการทั้งสองเข้าด้วยกัน การแยกรายการทำได้โดย(ที่นี่เราใช้ curried ด้วย) ผลที่ได้คือประเภท แทนการแยกผลลงในส่วนแรกและครั้งที่สองและรวมพวกเขาใช้เราสามารถทำเช่นนี้ได้โดยตรงโดย uncurrying เป็นpartition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

แท้จริงประเมิน(uncurry (++) . partition (< 10)) [4,12,11,1][4,1,12,11]

นอกจากนี้ยังมีข้อได้เปรียบทางทฤษฎีที่สำคัญ:

  1. ความดีความชอบเป็นสิ่งจำเป็นสำหรับภาษาที่ขาดชนิดข้อมูลและมีฟังก์ชั่นเท่านั้นเช่นแคลคูลัสแลมบ์ดา ในขณะที่ภาษาเหล่านี้ไม่ได้มีประโยชน์สำหรับการใช้งานจริงพวกเขามีความสำคัญมากจากมุมมองทางทฤษฎี
  2. สิ่งนี้เชื่อมต่อกับคุณสมบัติที่สำคัญของภาษาที่ใช้งานได้ - ฟังก์ชั่นเป็นวัตถุชั้นหนึ่ง ที่เราได้เห็นการแปลงจาก(a, b) -> cไปหมายความว่าผลการทำงานของหลังเป็นประเภทa -> (b -> c) b -> cกล่าวอีกนัยหนึ่งผลลัพธ์คือฟังก์ชัน
  3. (ยกเลิก) การเชื่อมต่อกับหมวดหมู่ปิดคาร์ทีเซียนปิดแกงซึ่งเป็นวิธีที่เด็ดขาดในการดูแลมบ์ดาพิมพ์นิ่ว

สำหรับบิต "โค้ดที่อ่านได้น้อยลงมาก" ควรเป็นเช่นนั้นmem x lst = any (\y -> y == x) lstหรือ (ด้วยเครื่องหมายแบ็กสแลช)
stusmith

ใช่ขอบคุณที่ชี้ให้เห็นว่าฉันจะแก้ไขให้ถูกต้อง
Petr Pudlák

9

แกงไม่ได้เป็นเพียงแค่วากยสัมพันธ์น้ำตาลทราย!

พิจารณาประเภทของลายเซ็นของadd1(uncurried) และadd2(curried):

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

(ในทั้งสองกรณีวงเล็บในลายเซ็นประเภทเป็นตัวเลือก แต่ฉันได้รวมไว้เพื่อความชัดเจน)

add1เป็นฟังก์ชั่นที่ใช้เวลา 2 tuple ของintและและส่งกลับ int เป็นฟังก์ชั่นที่ใช้เวลาอีกด้วยและส่งกลับฟังก์ชันอื่นที่ในการเปิดใช้เวลาและการส่งกลับintadd2intintint

ความแตกต่างที่สำคัญระหว่างทั้งสองจะปรากฏให้เห็นได้มากขึ้นเมื่อเราระบุแอปพลิเคชันฟังก์ชันอย่างชัดเจน มากำหนดฟังก์ชั่น (ไม่ curried) ที่ใช้อาร์กิวเมนต์แรกกับอาร์กิวเมนต์ที่สอง:

apply(f, b) = f b

ตอนนี้เราสามารถเห็นความแตกต่างระหว่างadd1และadd2ชัดเจนขึ้น add1ถูกเรียกด้วย 2-tuple:

apply(add1, (3, 5))

แต่add2ถูกเรียกด้วยint และจากนั้นค่าส่งคืนจะถูกเรียกด้วยอันอื่นint :

apply(apply(add2, 3), 5)

แก้ไข: ประโยชน์ที่สำคัญของการแกงคือคุณได้รับใบสมัครบางส่วนฟรี ให้บอกว่าคุณต้องการฟังก์ชั่นชนิดint -> int(พูดถึงmapรายการ) ที่เพิ่ม 5 เข้ากับพารามิเตอร์ คุณสามารถเขียนaddFiveToParam x = x+5หรือคุณอาจจะทำเทียบเท่ากับแลมบ์ดาอินไลน์ แต่คุณยังสามารถได้อย่างง่ายดายมากขึ้น (โดยเฉพาะในกรณีที่น่ารำคาญน้อยกว่านี้) เขียนadd2 5!


3
ฉันเข้าใจว่ามีตัวอย่างที่แตกต่างกันมากมายสำหรับฉากของฉัน แต่ผลลัพธ์ดูเหมือนจะเป็นการเปลี่ยนแปลงทางวากยสัมพันธ์ง่ายๆ
นักวิทยาศาสตร์บ้า

5
การแกงไม่ใช่แนวคิดที่ลึกซึ้งมาก มันเป็นเรื่องเกี่ยวกับการทำให้แบบจำลองพื้นฐานง่ายขึ้น (ดูแลมบ์ดาแคลคูลัส) หรือในภาษาที่มีสิ่งอันดับ อย่าดูถูกดูแคลนความสำคัญของความสะดวกสบายของวากยสัมพันธ์
Peaker

9

การแกงเป็นแค่น้ำตาลทราย แต่คุณเข้าใจผิดเล็กน้อยว่าน้ำตาลทำอะไรฉันคิดว่า ยกตัวอย่างของคุณ

fun add x y = x + y

จริงๆแล้วน้ำตาลในประโยคนั้นมีไว้สำหรับ

fun add x = fn y => x + y

นั่นคือ (เพิ่ม x) ส่งคืนฟังก์ชันที่รับอาร์กิวเมนต์ y และเพิ่ม x เป็น y

fun addTuple (x, y) = x + y

นั่นคือฟังก์ชั่นที่ใช้สิ่งอันดับและเพิ่มองค์ประกอบของมัน ทั้งสองฟังก์ชั่นนั้นแตกต่างกันมาก พวกเขาขัดแย้งกัน

หากคุณต้องการเพิ่ม 2 หมายเลขทั้งหมดในรายการ:

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

ผลที่ได้ก็[3,4,5]คือ

หากคุณต้องการที่จะหาผลรวมของ tuple แต่ละอันในรายการในทางกลับกันฟังก์ชัน addTuple นั้นลงตัวพอดี

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

ผลที่ได้ก็[12,13,14]คือ

ฟังก์ชั่น Curried นั้นยอดเยี่ยมซึ่งแอพพลิเคชั่นบางส่วนมีประโยชน์เช่นแผนที่, พับ, แอพ, ตัวกรอง พิจารณาฟังก์ชั่นนี้ซึ่งจะส่งกลับตัวเลขบวกที่ใหญ่ที่สุดในรายการที่ให้มาหรือ 0 ถ้าไม่มีตัวเลขบวก:

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

1
ฉันเข้าใจว่าฟังก์ชัน curried มีลายเซ็นประเภทที่แตกต่างกันและจริง ๆ แล้วเป็นฟังก์ชันที่ส่งคืนฟังก์ชันอื่น ฉันไม่มีส่วนบางส่วนของแอปพลิเคชัน
นักวิทยาศาสตร์บ้า

9

อีกสิ่งที่ฉันไม่ได้เห็นได้กล่าวถึงก็คือการที่การอนุญาตให้มีสิ่งที่เป็นนามธรรม

พิจารณาฟังก์ชั่นเหล่านี้ซึ่งเป็นส่วนหนึ่งของห้องสมุดของ 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

ในแต่ละกรณีตัวแปรชนิดcสามารถเป็นประเภทฟังก์ชันเพื่อให้ฟังก์ชันเหล่านี้ทำงานบนส่วนนำหน้าของรายการพารามิเตอร์ของอาร์กิวเมนต์ คุณไม่จำเป็นต้องมีคุณสมบัติพิเศษของภาษาในการสรุปฟังก์ชั่น arity หรือมีฟังก์ชั่นต่าง ๆ มากมายสำหรับความสามารถที่แตกต่างกัน


6

ความเข้าใจที่ จำกัด ของฉันคือ:

1) แอปพลิเคชันฟังก์ชันบางส่วน

แอปพลิเคชันฟังก์ชันบางส่วนเป็นกระบวนการส่งคืนฟังก์ชันที่ใช้จำนวนอาร์กิวเมนต์น้อยลง หากคุณให้ 2 จาก 3 ข้อโต้แย้งมันจะส่งคืนฟังก์ชันที่ใช้อาร์กิวเมนต์ 3-2 = 1 หากคุณระบุ 1 ใน 3 ข้อโต้แย้งมันจะส่งคืนฟังก์ชันที่ใช้อาร์กิวเมนต์ 3-1 = 2 หากคุณต้องการคุณสามารถใช้บางส่วนของอาร์กิวเมนต์ 3 จาก 3 และบางส่วนจะส่งคืนฟังก์ชันที่ไม่มีการโต้แย้ง

รับฟังก์ชั่นดังต่อไปนี้:

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

เมื่อผูก 1 ถึง x และนำบางส่วนไปใช้กับฟังก์ชั่นด้านบนf(x,y,z)คุณจะได้รับ:

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

ที่ไหน: f'(y,z) = 1 + y + z;

ตอนนี้ถ้าคุณผูก y กับ 2 และ z ถึง 3 และนำไปใช้บางส่วนf'(y,z)คุณจะได้รับ:

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

ที่ไหน: f''() = 1 + 2 + 3;

ตอนนี้ที่จุดใด ๆ ที่คุณสามารถเลือกที่จะประเมินผลการศึกษาf, หรือf' f''ดังนั้นฉันสามารถทำ:

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

หรือ

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

2) แกงกะหรี่

ความดีความชอบในมืออื่น ๆ เป็นกระบวนการของการแยกการทำงานเป็นห่วงโซ่ที่ซ้อนกันของฟังก์ชั่นการโต้แย้งอย่างใดอย่างหนึ่ง คุณไม่สามารถให้อาร์กิวเมนต์มากกว่า 1 รายการซึ่งเป็นหนึ่งหรือศูนย์

รับฟังก์ชั่นเดียวกันดังนั้น:

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

หากคุณแก้ปัญหาคุณจะได้ 3 ฟังก์ชัน:

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

ที่ไหน:

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

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

f'''(z) = z;

ตอนนี้ถ้าคุณโทรf'(x)ด้วยx = 1:

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

คุณถูกส่งคืนฟังก์ชันใหม่:

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

หากคุณโทรg(y)ด้วยy = 2:

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

คุณถูกส่งคืนฟังก์ชันใหม่:

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

ในที่สุดถ้าคุณโทรh(z)ด้วยz = 3:

h(3) = 1 + 2 + 3;

คุณกลับมา6แล้ว

3) การปิด

ในที่สุดการปิดคือกระบวนการของการจับฟังก์ชั่นและข้อมูลร่วมกันเป็นหน่วยเดียว ฟังก์ชั่นการปิดสามารถใช้ 0 เป็นจำนวนอาร์กิวเมนต์ไม่สิ้นสุด แต่ก็ยังรับรู้ถึงข้อมูลที่ไม่ได้ผ่านไป

อีกครั้งรับฟังก์ชั่นเดียวกัน:

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

คุณสามารถเขียนปิด:

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

ที่ไหน:

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

f'xปิดให้บริการใน หมายความว่าf'สามารถอ่านค่าของ x fที่อยู่ภายใน

ดังนั้นถ้าคุณจะโทรfด้วยx = 1:

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

คุณจะได้รับการปิด:

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

ตอนนี้ถ้าคุณโทรหาclosureOfFด้วยy = 2และz = 3:

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

ซึ่งจะกลับมา 6

ข้อสรุป

แอพพลิเคชั่นและการปิดบางส่วนมีลักษณะค่อนข้างคล้ายกันเมื่อพวกมันย่อยสลายฟังก์ชั่นออกเป็นส่วน ๆ มากขึ้น

การแยกย่อยสลายฟังก์ชันของอาร์กิวเมนต์หลายตัวเป็นฟังก์ชันซ้อนกันของอาร์กิวเมนต์เดี่ยวที่ส่งคืนฟังก์ชันของอาร์กิวเมนต์เดี่ยว ไม่มีประเด็นใดที่จะปิดกั้นฟังก์ชั่นการโต้แย้งอย่างน้อยหนึ่งข้อเนื่องจากมันไม่สมเหตุสมผล

แอปพลิเคชั่นบางส่วนจะสลายฟังก์ชันของอาร์กิวเมนต์หลายตัวเป็นฟังก์ชันของอาร์กิวเมนต์ที่น้อยกว่าซึ่งตอนนี้อาร์กิวเมนต์ที่ขาดหายไปจะถูกแทนที่ด้วยค่าที่ให้มา

การปิดการสลายตัวของฟังก์ชั่นเป็นฟังก์ชั่นและชุดข้อมูลที่ตัวแปรภายในฟังก์ชั่นที่ไม่ได้ผ่านสามารถดูภายในชุดข้อมูลเพื่อหาค่าที่จะผูกกับเมื่อถามเพื่อประเมิน

สิ่งที่ทำให้เกิดความสับสนเกี่ยวกับสิ่งเหล่านี้คือพวกเขาสามารถนำมาใช้เพื่อนำส่วนย่อยของคนอื่นมาใช้ ดังนั้นโดยสรุปแล้วพวกเขาทั้งหมดมีรายละเอียดเล็กน้อยเกี่ยวกับการใช้งาน พวกเขาทั้งหมดให้ค่าที่คล้ายกันในที่ที่คุณไม่จำเป็นต้องรวบรวมค่าทั้งหมดล่วงหน้าและในที่คุณสามารถนำมาใช้เป็นส่วนหนึ่งของฟังก์ชั่นเนื่องจากคุณได้สลายมันเป็นหน่วยที่สุขุม

การเปิดเผย

ฉันไม่ได้เป็นผู้เชี่ยวชาญในหัวข้อฉันเพิ่งเริ่มเรียนรู้เกี่ยวกับสิ่งเหล่านี้และดังนั้นฉันจึงให้ความเข้าใจในปัจจุบันของฉัน แต่อาจมีข้อผิดพลาดที่ฉันเชิญคุณให้ชี้ให้เห็นและจะแก้ไขตาม / ถ้า ฉันค้นพบใด ๆ


1
ดังนั้นคำตอบคือ: การแกงไม่มีประโยชน์อะไร?
ceving

1
@ แก้เท่าที่ฉันรู้ว่าถูกต้อง ในทางปฏิบัติการเรียนการสอนและการประยุกต์บางส่วนจะให้ประโยชน์เหมือนกัน ทางเลือกที่จะนำไปใช้ในภาษานั้นถูกสร้างขึ้นมาเพื่อเหตุผลในการนำไปใช้งานอย่างใดอย่างหนึ่งอาจจะง่ายกว่าที่จะใช้งานและจากนั้นอีกคนหนึ่งที่ได้รับภาษาที่แน่นอน
Didier A.

5

Currying (แอปพลิเคชั่นบางส่วน) ให้คุณสร้างฟังก์ชั่นใหม่จากฟังก์ชั่นที่มีอยู่โดยแก้ไขพารามิเตอร์บางอย่าง มันเป็นกรณีพิเศษของการปิดคำศัพท์ที่ฟังก์ชั่นที่ไม่ระบุชื่อเป็นเพียงเสื้อคลุมเล็ก ๆ น้อย ๆ ที่ส่งผ่านข้อโต้แย้งที่ถูกจับไปยังฟังก์ชั่นอื่น นอกจากนี้เรายังสามารถทำได้โดยใช้ไวยากรณ์ทั่วไปสำหรับการปิดคำศัพท์ แต่การใช้งานบางส่วนให้น้ำตาล syntactic ง่าย

นี่คือเหตุผลที่โปรแกรมเมอร์เสียงกระเพื่อมเมื่อทำงานในรูปแบบการทำงานบางครั้งใช้ห้องสมุดสำหรับการใช้งานบางส่วน

แทน(lambda (x) (+ 3 x))ซึ่งจะช่วยให้เราฟังก์ชั่นที่เพิ่ม 3 ถึงข้อโต้แย้งของคุณสามารถเขียนสิ่งที่ต้องการ(op + 3)และเพื่อเพิ่ม 3 ถึงองค์ประกอบของทุกบางรายการก็จะเป็นมากกว่า(mapcar (op + 3) some-list) (mapcar (lambda (x) (+ 3 x)) some-list)นี้opแมโครจะทำให้คุณฟังก์ชั่นซึ่งจะมีข้อโต้แย้งบางและจะเรียกx y z ...(+ a x y z ...)

ในภาษาที่ใช้งานได้จริงจำนวนมากแอปพลิเคชันบางส่วนจะถูกฝังเข้าไปในไวยากรณ์เพื่อให้ไม่มีopโอเปอเรเตอร์ ในการทริกเกอร์แอปพลิเคชั่นบางส่วนคุณเพียงเรียกใช้ฟังก์ชันที่มีอาร์กิวเมนต์น้อยกว่าที่ต้องการ แทนที่จะสร้าง"insufficient number of arguments"ข้อผิดพลาดผลลัพธ์คือฟังก์ชันของอาร์กิวเมนต์ที่เหลือ


"ความดีความชอบ ... ช่วยให้คุณสร้างฟังก์ชั่นใหม่ ... โดยกำหนดพารามิเตอร์บางอย่าง" - ไม่มีฟังก์ชั่นชนิดa -> b -> cไม่มีพารามิเตอร์s (พหูพจน์) cก็มีเพียงหนึ่งพารามิเตอร์ a -> bเมื่อเรียกมันกลับฟังก์ชั่นของชนิด
Max Heiber

4

สำหรับฟังก์ชั่น

fun add(x, y) = x + y

มันเป็นรูปแบบ f': 'a * 'b -> 'c

ในการประเมินหนึ่งจะทำ

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

สำหรับฟังก์ชั่น curried

fun add x y = x + y

ในการประเมินหนึ่งจะทำ

add 3
val it = fn : int -> int

โดยที่เป็นการคำนวณบางส่วนโดยเฉพาะ (3 + y) ซึ่งจะทำให้การคำนวณเสร็จสมบูรณ์ด้วย

it 5
val it = 8 : int

เพิ่มในกรณีที่สองเป็นของแบบฟอร์ม f: 'a -> 'b -> 'c

สิ่งที่การทำแกงที่นี่คือการเปลี่ยนฟังก์ชั่นที่ใช้สองข้อตกลงเป็นหนึ่งที่ใช้เวลาเพียงหนึ่งผลตอบแทน การประเมินบางส่วน

ทำไมหนึ่งต้องนี้

พูดxกับ RHS ไม่ใช่แค่ int ทั่วไป แต่แทนที่จะเป็นการคำนวณที่ซับซ้อนซึ่งใช้เวลาสักครู่จึงจะเสร็จสมบูรณ์เพื่อเพิ่มให้สาเกสองวินาที

x = twoSecondsComputation(z)

ดังนั้นฟังก์ชั่นตอนนี้ดูเหมือนว่า

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

ของประเภท add : int * int -> int

ตอนนี้เราต้องการคำนวณฟังก์ชั่นนี้สำหรับช่วงตัวเลขลองแมปกัน

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

สำหรับข้างต้นผลลัพธ์ของการtwoSecondsComputationประเมินทุกครั้ง ซึ่งหมายความว่าใช้เวลา 6 วินาทีในการคำนวณนี้

การใช้การผสมผสานระหว่างการจัดเตรียมและการรวมเป็นหนึ่งสามารถหลีกเลี่ยงได้

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

จากรูปแบบแกงกะหรี่ add : int -> int -> int

ตอนนี้เราสามารถทำได้

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

twoSecondsComputationความต้องการเท่านั้นที่จะได้รับการประเมินในครั้งเดียว ในการเพิ่มระดับให้เปลี่ยนสองวินาทีด้วย 15 นาทีหรือชั่วโมงใดก็ได้จากนั้นมีแผนที่เทียบกับ 100 หมายเลข

สรุป : การ Currying นั้นยอดเยี่ยมเมื่อใช้กับวิธีอื่นสำหรับฟังก์ชั่นระดับสูงกว่าเป็นเครื่องมือในการประเมินผลบางส่วน จุดประสงค์ของมันไม่สามารถพิสูจน์ได้ด้วยตนเอง


3

การดัดผมช่วยให้องค์ประกอบของฟังก์ชั่นมีความยืดหยุ่น

ฉันทำฟังก์ชั่น "แกง" ในบริบทนี้ฉันไม่สนใจประเภทของตัวบันทึกที่ฉันได้รับหรือที่มาของมัน ฉันไม่สนใจว่าการกระทำนั้นมาจากไหน ทั้งหมดที่ฉันสนใจคือการประมวลผลข้อมูลของฉัน

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.

ตัวแปรตัวสร้างเป็นฟังก์ชันที่ส่งคืนฟังก์ชันที่ส่งคืนฟังก์ชันที่รับอินพุตของฉันที่ทำงานของฉัน นี่เป็นตัวอย่างที่มีประโยชน์อย่างง่ายและไม่ใช่วัตถุที่มองเห็น


2

การแช่งเป็นข้อได้เปรียบเมื่อคุณไม่มีอาร์กิวเมนต์ทั้งหมดสำหรับฟังก์ชัน หากคุณบังเอิญประเมินฟังก์ชั่นอย่างเต็มที่แสดงว่าไม่มีความแตกต่างที่สำคัญ

การแกงช่วยให้คุณหลีกเลี่ยงการพูดถึงพารามิเตอร์ที่ยังไม่ได้กำหนด มันกระชับกว่าและไม่ต้องค้นหาชื่อพารามิเตอร์ที่ไม่ได้ชนกับตัวแปรอื่นในขอบเขต (ซึ่งเป็นประโยชน์ที่ฉันโปรดปราน)

ตัวอย่างเช่นเมื่อใช้ฟังก์ชั่นที่ทำหน้าที่เป็นอาร์กิวเมนต์คุณมักจะพบว่าตัวเองอยู่ในสถานการณ์ที่คุณต้องการฟังก์ชั่นเช่น "เพิ่ม 3 เพื่อป้อนข้อมูล" หรือ "เปรียบเทียบอินพุตกับตัวแปร v" ด้วย currying ฟังก์ชั่นเหล่านี้ถูกเขียนได้อย่างง่ายดาย: และadd 3 (== v)โดยไม่ต้องความดีความชอบที่คุณต้องใช้การแสดงออกแลมบ์ดา: และx => add 3 x x => x == vการแสดงออกแลมบ์ดานั้นมีความยาวเป็นสองเท่าและมีงานยุ่งเล็กน้อยที่เกี่ยวข้องกับการเลือกชื่อนอกเหนือจากxถ้ามีxขอบเขตอยู่แล้ว

ข้อดีอีกด้านของภาษาที่อิงกับการปิดบังคือเมื่อคุณเขียนโค้ดทั่วไปสำหรับฟังก์ชั่นคุณจะไม่ต้องลงเอยด้วยตัวแปรหลายร้อยตัวตามจำนวนพารามิเตอร์ ตัวอย่างเช่นใน C # วิธีการ 'แกง' จะต้องมีชุดตัวเลือกสำหรับ Func <R>, Func <A, R>, Func <A1, A2, R>, Func <A1, A2, A3, R> และอื่น ๆ ตลอดไป ใน Haskell ค่าเทียบเท่าของ Func <A1, A2, R> นั้นเหมือนกับ Func <Tuple <A1, A2>, R> หรือ Func <A1, Func <A2, R >> (และ Func <R> เป็นเหมือน Func <Unit, R>) ดังนั้นตัวแปรทั้งหมดจะตรงกับเคส Func <A, R> เดี่ยว


2

เหตุผลหลักที่ฉันสามารถนึกถึง (และฉันไม่ใช่ผู้เชี่ยวชาญในเรื่องนี้ด้วยวิธีการใด ๆ ) เริ่มแสดงให้เห็นถึงประโยชน์ของมันเมื่อฟังก์ชั่นเปลี่ยนจากเรื่องไร้สาระไปเป็นเรื่องไม่สำคัญ ในทุกกรณีเล็กน้อยที่มีแนวคิดส่วนใหญ่เกี่ยวกับลักษณะนี้คุณจะไม่พบประโยชน์ที่แท้จริง อย่างไรก็ตามภาษาที่ใช้งานได้ส่วนใหญ่จะใช้ประโยชน์อย่างมากของสแต็กในการดำเนินการประมวลผล พิจารณาPostScriptหรือLispเป็นตัวอย่างของสิ่งนี้ ด้วยการใช้การแกงทำให้สามารถซ้อนฟังก์ชันได้อย่างมีประสิทธิภาพมากขึ้นและประโยชน์นี้จะชัดเจนขึ้นเมื่อการดำเนินการเติบโตน้อยลงเล็กน้อย ในลักษณะ curried คำสั่งและอาร์กิวเมนต์สามารถถูกโยนลงบนสแต็กตามลำดับและแตกออกได้ตามต้องการเพื่อให้ทำงานในลำดับที่เหมาะสม


1
การสร้างสแต็กเฟรมให้มีประสิทธิภาพมากขึ้นเป็นอย่างไรกันแน่
Mason Wheeler

1
@MasonWheeler: ฉันไม่รู้เหมือนที่ฉันบอกว่าฉันไม่ใช่ผู้เชี่ยวชาญเกี่ยวกับภาษาที่ใช้งานได้ ฉันติดป้ายกำกับชุมชนนี้เป็นพิเศษเพราะเหตุนี้
Joel Etherton

4
@MasonWheeler คุณมีจุดที่จะใช้ถ้อยคำของคำตอบนี้ แต่ขอให้ฉันชิปและพูดว่าจำนวนของเฟรมสแต็คที่สร้างขึ้นจริงขึ้นอยู่กับการใช้งานมาก ตัวอย่างเช่นในเครื่อง G ที่ไม่มีแท็กไม่มีสปิน (STG วิธีที่ GHC ใช้ Haskell) ทำให้การประเมินผลล่าช้าจริงจนกว่าจะรวบรวมอาร์กิวเมนต์ทั้งหมด (หรืออย่างน้อยก็มากเท่าที่จำเป็นต้องรู้) อาร์กิวเมนต์ ฉันไม่สามารถจำได้ว่ามันจะทำสำหรับทุกฟังก์ชั่นหรือเฉพาะสำหรับตัวสร้าง แต่ฉันคิดว่ามันควรจะเป็นไปได้สำหรับฟังก์ชั่นส่วนใหญ่ (จากนั้นอีกครั้งแนวคิดของ "กรอบสแต็ก" ไม่ได้ใช้กับ STG จริงๆ)

1

การปิดตาขึ้นอยู่กับความสามารถในการส่งคืนฟังก์ชัน

พิจารณารหัสหลอกนี้ (ประดิษฐ์)

var f = (m, x, b) => ... ส่งคืนบางสิ่ง ...

มากำหนดเงื่อนไขว่าการเรียก f ที่มีอาร์กิวเมนต์น้อยกว่าสามรายการส่งคืนฟังก์ชัน

var g = f (0, 1); // นี่จะส่งคืนฟังก์ชันที่ผูกไว้กับ 0 และ 1 (m และ x) ที่ยอมรับอาร์กิวเมนต์มากกว่าหนึ่ง (b)

var y = g (42); // เรียก g ด้วยอาร์กิวเมนต์ที่สามที่หายไปโดยใช้ 0 และ 1 สำหรับ m และ x

การที่คุณสามารถใช้ข้อโต้แย้งบางส่วนและกลับมาฟังก์ชั่นที่ใช้งานได้อีกครั้ง (ผูกพันกับข้อโต้แย้งที่คุณได้จัดหา) ค่อนข้างมีประโยชน์ (และ DRY)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.