"การยก" ใน Haskell คืออะไร


138

ฉันไม่เข้าใจว่า "การยก" คืออะไร ฉันควรเข้าใจพระก่อนหรือไม่ก่อนที่จะเข้าใจว่า "ลิฟต์" คืออะไร? (ฉันไม่รู้เกี่ยวกับพระอย่างสมบูรณ์เช่นกัน :) หรือใครบางคนสามารถอธิบายให้ฉันด้วยคำง่ายๆ?


9
อาจมีประโยชน์อาจจะไม่ใช่: haskell.org/haskellwiki/Lifting
kennytm

คำตอบ:


179

การยกเป็นรูปแบบการออกแบบมากกว่าแนวคิดทางคณิตศาสตร์ (แม้ว่าฉันคาดว่าจะมีใครบางคนอยู่ที่นี่ในตอนนี้จะหักล้างฉันด้วยการแสดงว่าลิฟต์เป็นหมวดหมู่หรือบางอย่าง)

โดยทั่วไปคุณมีชนิดข้อมูลพร้อมพารามิเตอร์ สิ่งที่ต้องการ

data Foo a = Foo { ...stuff here ...}

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

liftFoo2 :: (a -> b -> c) -> Foo a -> Foo b -> Foo c

กล่าวอีกนัยหนึ่งคุณมีฟังก์ชั่นที่รับฟังก์ชั่นสองอาร์กิวเมนต์ (เช่น(+)โอเปอเรเตอร์) และเปลี่ยนเป็นฟังก์ชันเทียบเท่าสำหรับ Foos

ดังนั้นตอนนี้คุณสามารถเขียน

addFoo = liftFoo2 (+)

แก้ไข: ข้อมูลเพิ่มเติม

แน่นอนคุณสามารถมีliftFoo3, liftFoo4และอื่น ๆ อย่างไรก็ตามสิ่งนี้มักไม่จำเป็น

เริ่มด้วยการสังเกต

liftFoo1 :: (a -> b) -> Foo a -> Foo b

fmapแต่นั่นคือสิ่งเดียวกับ ดังนั้นแทนที่จะliftFoo1เขียน

instance Functor Foo where
   fmap f foo = ...

หากคุณต้องการให้ระเบียบสมบูรณ์คุณสามารถพูดได้

liftFoo1 = fmap

หากคุณสามารถFooเป็นนักแสดงได้บางทีคุณอาจทำให้เป็นนักแสดงนำได้ ที่จริงแล้วถ้าคุณสามารถเขียนได้liftFoo2อินสแตนซ์ของการสมัครจะมีลักษณะดังนี้:

import Control.Applicative

instance Applicative Foo where
   pure x = Foo $ ...   -- Wrap 'x' inside a Foo.
   (<*>) = liftFoo2 ($)

ตัว(<*>)ดำเนินการสำหรับ Foo มีชนิด

(<*>) :: Foo (a -> b) -> Foo a -> Foo b

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

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

และในทำนองเดียวกันก็มีliftAและliftA3. แต่คุณไม่ได้ใช้บ่อยนักเพราะมีผู้ให้บริการรายอื่น

(<$>) = fmap

ให้คุณเขียน:

result = myFunction <$> arg1 <*> arg2 <*> arg3 <*> arg4

คำนี้myFunction <$> arg1จะคืนค่าฟังก์ชันใหม่ที่หุ้มด้วย Foo ในทางกลับกันนี้สามารถนำไปใช้กับการโต้แย้งต่อไปโดยใช้(<*>)และอื่น ๆ ดังนั้นตอนนี้แทนที่จะมีฟังก์ชั่นการยกสำหรับทุก arity คุณแค่มีโซ่ของเดซี่


26
มันอาจจะคุ้มค่าการเตือนว่าลิฟท์ควรเคารพกฎหมายมาตรฐานและlift id == id lift (f . g) == (lift f) . (lift g)
Carlos Scheidegger

13
ลิฟต์เป็น "หมวดหมู่หรืออะไรบางอย่าง" Carlos ได้แสดงรายการกฎหมายของ Functor ที่idและ.องค์ประกอบลูกศรและลูกศรของบางประเภทตามลำดับ โดยปกติเมื่อพูดถึง Haskell หมวดหมู่ที่มีปัญหาคือ "Hask" ซึ่งลูกศรเป็นฟังก์ชัน Haskell (กล่าวอีกนัยหนึ่งidและ.อ้างอิงถึงฟังก์ชัน Haskell ที่คุณรู้จักและชื่นชอบ)
Dan Burton

3
นี้ควรอ่านinstance Functor Fooไม่ได้instance Foo Functorใช่มั้ย? ฉันจะแก้ไขด้วยตัวเอง แต่ฉันไม่แน่ใจ 100%
amalloy

2
การยกโดยไม่มีการใช้งานคือ = Functor ฉันหมายถึงคุณมี 2 ทางเลือกคือ Functor หรือ Applicative Functor ยกแรกพารามิเตอร์เดียวฟังก์ชั่นฟังก์ชั่นหลายพารามิเตอร์ที่สอง ค่อนข้างมากแค่นั้นแหละ ขวา? มันไม่ใช่วิทยาศาสตร์จรวด :) มันฟังดูเหมือนมัน ขอบคุณสำหรับคำตอบที่ดี btw!
jhegedus

2
@atc: นี่เป็นแอปพลิเคชั่นบางส่วน ดูwiki.haskell.org/Partial_application
Paul Johnson

41

คำอธิบายของ Paul และ yairchu เป็นสิ่งที่ดี

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

liftFoo1 :: (a -> b) -> Foo a -> Foo b

โดยทั่วไปแล้วการยกฟังก์ชั่นที่รับอาร์กิวเมนต์ 1 จะถูกจับในคลาสประเภทFunctorและการดำเนินการยกจะเรียกว่าfmap:

fmap :: Functor f => (a -> b) -> f a -> f b

สังเกตความคล้ายคลึงกันกับliftFoo1ประเภทของ ในความเป็นจริงถ้าคุณมีliftFoo1คุณสามารถทำFooตัวอย่างของFunctor:

instance Functor Foo where
  fmap = liftFoo1

นอกจากนี้หลักเกณฑ์ในการยกไปจำนวนข้อของการขัดแย้งที่เรียกว่าสไตล์ applicative อย่าไปสนใจเรื่องนี้จนกว่าคุณจะเข้าใจการยกของฟังก์ชั่นด้วยจำนวนอาร์กิวเมนต์ที่แน่นอน แต่เมื่อคุณเรียนรู้คุณ Haskellมีบทที่ดีในเรื่องนี้ Typeclassopediaเป็นอีกหนึ่งเอกสารที่ดีที่จะอธิบายFunctorและapplicative (เช่นเดียวกับการเรียนประเภทอื่น ๆ เลื่อนลงไปที่บทในเอกสารนั้น)

หวังว่านี่จะช่วยได้!


25

มาเริ่มด้วยตัวอย่างกัน (เพิ่มพื้นที่สีขาวเพื่อการนำเสนอที่ชัดเจนขึ้น):

> import Control.Applicative
> replicate 3 'a'
"aaa"
> :t replicate
replicate        ::         Int -> b -> [b]
> :t liftA2
liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)
> :t liftA2 replicate
liftA2 replicate :: (Applicative f) =>       f Int -> f b -> f [b]
> (liftA2 replicate) [1,2,3] ['a','b','c']
["a","b","c","aa","bb","cc","aaa","bbb","ccc"]
> ['a','b','c']
"abc"

liftA2เปลี่ยนฟังก์ชั่นของประเภทธรรมดาเป็นฟังก์ชั่นประเภทเดียวกันที่ห่อหุ้มในApplicativeรายการเช่นรายการIOฯลฯ

อีกยกที่พบบ่อยคือจากlift Control.Monad.Transมันเปลี่ยนการกระทำของ monadic หนึ่ง monad เป็นการกระทำของ monad ที่ถูกแปลง

โดยทั่วไปแล้ว "การยก" จะยกฟังก์ชั่น / แอ็คชั่นให้เป็นแบบ "พัน" (ดังนั้นฟังก์ชันดั้งเดิมจะสามารถทำงานได้ "ภายใต้การแรป")

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


13

การยกเป็นแนวคิดที่ให้คุณเปลี่ยนฟังก์ชั่นให้เป็นฟังก์ชั่นที่สอดคล้องกันภายในการตั้งค่าอื่น

ลองดูที่http://haskell.org/haskellwiki/Lifting


40
ใช่ แต่หน้านั้นเริ่มต้น "เรามักจะเริ่มต้นด้วย functor (covariant) ... " ไม่เป็นมือใหม่อย่างแน่นอน
พอลจอห์นสัน

3
แต่ "functor" มีการเชื่อมโยงดังนั้น newbie สามารถคลิกเพื่อดูว่า Functor คืออะไร เป็นที่ยอมรับหน้าเชื่อมโยงไม่ดีนัก ฉันต้องได้รับบัญชีและแก้ไข
jrockway

10
มันเป็นปัญหาที่ฉันเคยเห็นในเว็บไซต์การเขียนโปรแกรมการทำงานอื่น ๆ ; แต่ละแนวคิดมีการอธิบายในแง่ของแนวคิดอื่น ๆ (ไม่คุ้นเคย) แนวคิดจนกระทั่งมือใหม่ไปครบวงจร (และรอบโค้ง) จะต้องเป็นสิ่งที่ต้องทำด้วยความชื่นชอบการสอบถามซ้ำ
DNA

2
โหวตให้กับลิงค์นี้ ลิฟต์ทำให้การเชื่อมต่อระหว่างโลกหนึ่งกับอีกโลกหนึ่ง
eccstartup

3
คำตอบเช่นนี้ดีเมื่อคุณเข้าใจหัวข้อ
doubleOrt

-2

ตามที่กวดวิชาเงานี้เป็น functor เป็นภาชนะบางคน (เช่นMaybe<a>, List<a>หรือTree<a>ที่สามารถเก็บองค์ประกอบของบางประเภทอื่นa) ฉันได้ใช้สัญกรณ์ Java generics, <a>, สำหรับประเภทองค์ประกอบและคิดว่าขององค์ประกอบที่เป็นผลเบอร์รี่บนต้นไม้a Tree<a>มีฟังก์ชั่นfmapซึ่งจะมีฟังก์ชั่นองค์ประกอบแปลงและภาชนะบรรจุa->b functor<a>ซึ่งจะใช้กับองค์ประกอบของภาชนะทุกอย่างมีประสิทธิภาพแปลงมันเป็นa->b functor<b>เมื่อเพียงอาร์กิวเมนต์แรกจะถูกส่งให้a->b, รอfmap functor<a>นั่นคือการจัดหาa->bเพียงอย่างเดียวเปลี่ยนฟังก์ชันระดับองค์ประกอบนี้เป็นฟังก์ชันfunctor<a> -> functor<b>ที่ทำงานบนคอนเทนเนอร์ สิ่งนี้เรียกว่าการยกของฟังก์ชั่น เนื่องจากตู้คอนเทนเนอร์นั้นเรียกว่าfunctorผู้ Fun แทนที่จะเป็น Monads เป็นสิ่งที่จำเป็นสำหรับการยก พระเป็นประเภทของ "ขนาน" เพื่อยก ทั้งสองต้องพึ่งพาความคิด Functor f<a> -> f<b>และทำ แตกต่างก็คือการยกใช้a->bสำหรับการแปลงขณะ Monad a -> f<b>ต้องใช้ในการกำหนด


5
ฉันให้คุณทำเครื่องหมายลงเพราะ "functor is some container" คือเหยื่อล่อไฟที่หมุนรอบตัว ตัวอย่าง: ฟังก์ชั่นจากบางrประเภทเป็น (ใช้cเพื่อความหลากหลาย), คือ Functors พวกเขาไม่ได้ "มี" ใด ๆc's ในตัวอย่างนี้ fmap คือการจัดองค์ประกอบของฟังก์ชั่นการใช้a -> bฟังก์ชั่นและr -> aหนึ่งเพื่อให้คุณใหม่r -> bฟังก์ชั่น ยังไม่มีตู้คอนเทนเนอร์นอกจากนี้ถ้าทำได้ฉันจะทำเครื่องหมายลงอีกครั้งสำหรับประโยคสุดท้าย
BMeph

1
นอกจากนี้ยังfmapมีฟังก์ชั่นและไม่ต้อง "รอ" เพื่ออะไร "ตู้คอนเทนเนอร์" เป็น Functor เป็นจุดยก นอกจากนี้ Monads มีถ้ามีอะไรเป็นความคิดแบบคู่เพื่อยกที่: Monad ช่วยให้คุณใช้สิ่งที่ได้รับการยกบางจำนวนบวกครั้งราวกับว่ามันได้ถูกยกขึ้นเพียงครั้งเดียว - นี้เป็นที่รู้จักกันดีในฐานะแฟบ
BMeph

1
@BMeph To wait, to expect, to anticipateเป็นคำพ้องความหมาย โดยการพูดว่า "ฟังก์ชั่นรอ" ฉันหมายถึง "ฟังก์ชั่นคาดหวัง"
Val

@BMeph ฉันอยากจะบอกว่าแทนที่จะคิดว่าฟังก์ชั่นเป็นตัวอย่างของความคิดที่ว่า functors เป็นคอนเทนเนอร์คุณควรนึกถึงฟังก์ชั่นที่มีสติของฟังก์ชั่นเป็นตัวอย่างที่เป็นแนวคิดที่ว่าฟังก์ชั่นไม่ใช่คอนเทนเนอร์ ฟังก์ชั่นคือการจับคู่จากโดเมนไปยังโคโดเมนโดเมนที่เป็นผลคูณของพารามิเตอร์ทั้งหมดโคโดเมนเป็นประเภทเอาท์พุทของฟังก์ชัน ในทำนองเดียวกันรายการคือการแมปจาก Naturals ไปยังประเภทด้านในของรายการ (domain -> codomain) มันจะคล้ายกันมากขึ้นถ้าคุณจำฟังก์ชันหรือไม่เก็บรายการไว้
อัฒภาค

@BMeph หนึ่งในรายการเหตุผลเดียวที่คิดว่าเป็นเหมือนคอนเทนเนอร์คือในหลายภาษาพวกเขาสามารถกลายพันธุ์ในขณะที่ฟังก์ชั่นแบบดั้งเดิมไม่สามารถ แต่ใน Haskell นั่นไม่ใช่คำสั่งที่ยุติธรรมเพราะไม่สามารถกลายพันธุ์ได้และทั้งคู่สามารถคัดลอก - กลายพันธุ์: b = 5 : aและf 0 = 55 f n = g nทั้งคู่เกี่ยวข้องกับการปลอมแปลง "คอนเทนเนอร์" นอกจากนี้ความจริงที่ว่ารายการจะถูกเก็บไว้ในหน่วยความจำอย่างสมบูรณ์ในขณะที่ฟังก์ชั่นจะถูกจัดเก็บเป็นการคำนวณ แต่การบันทึกความจำ / รายการ monorphic ที่ไม่ได้จัดเก็บระหว่างการโทรทั้งสองจะทำให้เสียอึออกจากแนวคิดนั้น
อัฒภาค
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.