มีใครสามารถอธิบายฟังก์ชัน traverse ใน Haskell ได้หรือไม่?


99

ฉันพยายามและล้มเหลวในการ grok ฟังก์ชั่นจากtraverse Data.Traversableฉันไม่สามารถมองเห็นจุดของมัน เนื่องจากฉันมาจากภูมิหลังที่จำเป็นใครก็ได้ช่วยอธิบายให้ฉันฟังในแง่ของการวนซ้ำที่จำเป็นได้ไหม รหัสหลอกจะได้รับการชื่นชมมาก ขอบคุณ.


1
บทความThe Essence of the Iterator Patternอาจมีประโยชน์เนื่องจากสร้างแนวคิดของการสำรวจทีละขั้นตอน มีแนวคิดขั้นสูงบางอย่างที่นำเสนอ
Jackie

คำตอบ:


121

traverseเหมือนกับfmapยกเว้นว่าจะให้คุณเรียกใช้เอฟเฟกต์ในขณะที่คุณกำลังสร้างโครงสร้างข้อมูลใหม่

ดูตัวอย่างจากData.Traversableเอกสารประกอบ

 data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)

Functorตัวอย่างของการTreeจะเป็น:

instance Functor Tree where
  fmap f Empty        = Empty
  fmap f (Leaf x)     = Leaf (f x)
  fmap f (Node l k r) = Node (fmap f l) (f k) (fmap f r)

มันสร้างต้นไม้ทั้งหมดขึ้นใหม่โดยใช้fกับทุกค่า

instance Traversable Tree where
    traverse f Empty        = pure Empty
    traverse f (Leaf x)     = Leaf <$> f x
    traverse f (Node l k r) = Node <$> traverse f l <*> f k <*> traverse f r

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

สำหรับเหตุผลทางประวัติศาสตร์Traversableชั้นนอกจากนี้ยังมีรุ่นเอกของที่เรียกว่าtraverse mapMสำหรับ intents และวัตถุประสงค์mapMเป็นเช่นเดียวกับtraverse- มันมีอยู่เป็นวิธีการแยกจากกันเพราะApplicativeเพียง Monadแต่ต่อมากลายเป็นซุปเปอร์คลาสของ

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

Node.prototype.traverse = function (f) {
  return new Node(this.l.traverse(f), f(this.k), this.r.traverse(f));
}

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


11
คำว่า 'effect' หมายถึงอะไร?
missingfaktor

25
@missingfaktor: หมายถึงข้อมูลโครงสร้างของ a Functorส่วนที่ไม่ใช่พาราเมตริก ค่าสถานะในStateความล้มเหลวในMaybeและEitherจำนวนองค์ประกอบใน[]และแน่นอนผลข้างเคียงภายนอกโดยพลการในIO. ฉันไม่สนใจว่ามันเป็นคำทั่วไป (เช่นMonoidฟังก์ชันที่ใช้ "ว่าง" และ "ต่อท้าย" แนวคิดนี้จะกว้างกว่าคำที่แนะนำในตอนแรก) แต่เป็นเรื่องธรรมดาและตอบสนองวัตถุประสงค์ได้ดีพอ
CA McCann

@CA McCann: เข้าใจแล้ว ขอบคุณสำหรับคำตอบ!
missingfaktor

1
"ฉันค่อนข้างมั่นใจว่าคุณไม่ควรทำแบบนี้ [... ]" ไม่แน่นอน - มันจะน่ารังเกียจพอ ๆ กับการสร้างผลกระทบจากapผลลัพธ์ก่อนหน้านี้ ฉันได้ทบทวนคำพูดนั้นใหม่ตามนั้น
duplode

2
"การใช้งานเกือบจะเหมือนกับ monads ยกเว้นว่าผลกระทบไม่สามารถขึ้นอยู่กับผลลัพธ์ก่อนหน้านี้" ... มีหลายสิ่งหลายอย่างที่คลิกเข้ามาในบรรทัดนี้สำหรับฉันขอบคุณ!
กัม

58

traverseเปลี่ยนสิ่งที่อยู่ภายในให้Traversableกลายเป็นTraversableสิ่งของ "ภายใน" Applicativeและได้รับฟังก์ชันที่ทำให้Applicativeสิ่งต่างๆ

ใช้ Let 's Maybeเป็นและรายชื่อเป็นApplicative Traversableอันดับแรกเราต้องมีฟังก์ชันการเปลี่ยนแปลง:

half x = if even x then Just (x `div` 2) else Nothing

ดังนั้นถ้าจำนวนคือแม้เราได้รับครึ่งหนึ่งของมัน (ภายในJust) Nothingอื่นที่เราได้รับ หากทุกอย่างเป็นไปอย่าง "เรียบร้อย" จะมีลักษณะดังนี้:

traverse half [2,4..10]
--Just [1,2,3,4,5]

แต่...

traverse half [1..10]
-- Nothing

เหตุผลก็คือ<*>ฟังก์ชันถูกใช้เพื่อสร้างผลลัพธ์และเมื่อหนึ่งในอาร์กิวเมนต์คือNothingเราจะNothingกลับมา

ตัวอย่างอื่น:

rep x = replicate x x

ฟังก์ชั่นนี้จะสร้างรายชื่อของความยาวxที่มีเนื้อหาxเช่น=rep 3 [3,3,3]เป็นผลมาจากtraverse rep [1..3]อะไร?

เราได้รับผลบางส่วน[1], [2,2]และการใช้[3,3,3] repตอนนี้ความหมายของรายการในฐานะApplicativesเป็น "ใช้เวลารวมกันทุกคน" เช่นเป็น(+) <$> [10,20] <*> [3,4][13,14,23,24]

"รวมกันทั้งหมด" ของ[1]และเป็นครั้งที่สอง[2,2] [1,2]ทั้งหมดรวมกันของทั้งสองครั้ง[1,2]และเป็นครั้งที่หก[3,3,3] [1,2,3]ดังนั้นเราจึงมี:

traverse rep [1..3]
--[[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]

1
คุณท้ายผลทำให้ผมนึกถึงเรื่องนี้
hugomg

3
@missingno: ใช่พวกเขาพลาดfac n = length $ traverse rep [1..n]
Landei

1
จริงๆแล้วมันอยู่ภายใต้ "List-encoding-programmer" (แต่ใช้การทำความเข้าใจรายการ) เว็บไซต์นั้นครอบคลุม :)
hugomg

1
@missingno: หืมมมก็ไม่ได้ว่าเดียวกัน ... ทั้งสองจะอาศัยอยู่กับพฤติกรรมของสินค้า monad รายการคาร์ทีเซียน แต่เว็บไซต์ที่ใช้เพียงสองครั้งเพื่อให้มันมากขึ้นเช่นการทำกว่ารูปแบบทั่วไปมากขึ้นโดยใช้liftA2 (,) traverse
CA McCann

42

ฉันคิดว่ามันง่ายที่สุดในการทำความเข้าใจในแง่ของการsequenceAเป็นtraverseสามารถกำหนดดังต่อไปนี้

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
traverse f = sequenceA . fmap f

sequenceA จัดลำดับองค์ประกอบของโครงสร้างจากซ้ายไปขวาโดยส่งคืนโครงสร้างที่มีรูปร่างเดียวกันซึ่งมีผลลัพธ์

sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a)
sequenceA = traverse id

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

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

คุณยังสามารถเปรียบเทียบกับFoldableซึ่งกำหนดฟังก์ชันที่เกี่ยวข้องtraverse_ได้

traverse_ :: (Foldable t, Applicative f) => (a -> f b) -> t a -> f ()

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


ตัวอย่างง่ายๆของการใช้งานคือการใช้รายการเป็นโครงสร้างที่สามารถข้ามผ่านได้และIOเป็นการนำไปใช้:

λ> import Data.Traversable
λ> let qs = ["name", "quest", "favorite color"]
λ> traverse (\thing -> putStrLn ("What is your " ++ thing ++ "?") *> getLine) qs
What is your name?
Sir Lancelot
What is your quest?
to seek the holy grail
What is your favorite color?
blue
["Sir Lancelot","to seek the holy grail","blue"]

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


การสำรวจเป็นเพียงรูปแบบทั่วไปของ mapM หรือไม่? ในความเป็นจริงsequenceA . fmapสำหรับรายการเทียบเท่ากับsequence . mapมันไม่ใช่เหรอ?
Raskell

'ลำดับผลข้างเคียง' หมายความว่าอย่างไร? คำตอบของคุณ 'ผลข้างเคียง' คืออะไร - ฉันคิดว่าผลข้างเคียงเป็นไปได้เฉพาะใน monads เท่านั้น ความนับถือ.
Marek

1
@Marek "ฉันแค่คิดว่าผลข้างเคียงเป็นไปได้เฉพาะใน monads เท่านั้น" - การเชื่อมต่อหลวมกว่านั้นมาก: (1) สามารถใช้IO ประเภทนี้เพื่อแสดงผลข้างเคียง (2) IOเป็นโมนาดซึ่งกลายเป็นสิ่งที่สะดวกมาก Monads ไม่ได้เชื่อมโยงกับผลข้างเคียงเป็นหลัก นอกจากนี้ควรสังเกตว่ามีความหมายของ "ผล" ซึ่งกว้างกว่า "ผลข้างเคียง" ในความหมายปกติซึ่งรวมถึงการคำนวณที่บริสุทธิ์ ในประเด็นสุดท้ายนี้โปรดดูด้วยว่า“ มีผล” หมายความว่าอย่างไร
duplode

(โดยวิธีการที่ @hammar ฉันใช้เสรีภาพในการเปลี่ยน "ผลข้างเคียง" เป็น "ผล" ในคำตอบนี้เนื่องจากเหตุผลที่ระบุไว้ในความคิดเห็นด้านบน)
duplode

17

มันเหมือนกับfmapยกเว้นว่าคุณสามารถเรียกใช้เอฟเฟกต์ภายในฟังก์ชัน mapper ซึ่งจะเปลี่ยนประเภทผลลัพธ์ด้วย

[1, 2, 3]ลองนึกภาพรายการของจำนวนเต็มตัวแทนรหัสผู้ใช้ในฐานข้อมูล: หากคุณต้องการfmapใช้ ID ผู้ใช้เหล่านี้เป็นชื่อผู้ใช้คุณไม่สามารถใช้แบบดั้งเดิมfmapได้เนื่องจากภายในฟังก์ชันคุณต้องเข้าถึงฐานข้อมูลเพื่ออ่านชื่อผู้ใช้ (ซึ่งต้องใช้เอฟเฟกต์ - ในกรณีนี้โดยใช้IOmonad)

ลายเซ็นของtraverseคือ:

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)

ด้วยtraverseคุณสามารถสร้างเอฟเฟกต์ได้ดังนั้นโค้ดของคุณสำหรับการแมป ID ผู้ใช้กับชื่อผู้ใช้จะมีลักษณะดังนี้

mapUserIDsToUsernames :: (Num -> IO String) -> [Num] -> IO [String]
mapUserIDsToUsernames fn ids = traverse fn ids

นอกจากนี้ยังมีฟังก์ชันที่เรียกว่าmapM:

mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)

การใช้งานใด ๆmapMสามารถแทนที่traverseได้ แต่ไม่สามารถใช้วิธีอื่นได้ mapMใช้ได้กับ monads เท่านั้นในขณะที่traverseเป็นแบบทั่วไป

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


ฉันได้ปรับเปลี่ยนคำตอบของคุณด้วยเหตุผลเดียวกันกับที่ทำให้ฉันต้องแก้ไขฮัมมาร์
duplode

7

traverse คือลูป การใช้งานขึ้นอยู่กับโครงสร้างข้อมูลที่จะส่งผ่าน ที่อาจจะมีรายการต้นไม้Maybe, Seq(อิทธิพลต่อ) หรือสิ่งที่มีวิธีการทั่วไปของการถูกเดินทางข้ามผ่านสิ่งที่ต้องการสำหรับวงหรือฟังก์ชันเวียน อาร์เรย์จะมี for-loop, list a while-loop, tree ทั้งสิ่งที่เรียกซ้ำหรือการรวมกันของ stack กับ while-loop แต่ในภาษาที่ใช้งานได้คุณไม่จำเป็นต้องใช้คำสั่งลูปที่ยุ่งยากเหล่านี้: คุณรวมส่วนภายในของลูป (ในรูปของฟังก์ชัน) เข้ากับโครงสร้างข้อมูลในลักษณะที่ตรงกว่าและมีรายละเอียดน้อยกว่า

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

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