Ai đó có thể giải thích chức năng đi ngang trong Haskell không?


99

Tôi đang cố gắng và không tìm được traversechức năng từ Data.Traversable. Tôi không thể nhìn thấy điểm của nó. Vì tôi đến từ nền tảng mệnh lệnh, ai đó có thể vui lòng giải thích cho tôi về vòng lặp mệnh lệnh được không? Mã giả sẽ được đánh giá cao hơn nhiều. Cảm ơn.


1
Bài viết Bản chất của mô hình lặp lại có thể hữu ích vì nó xây dựng khái niệm đi ngang từng bước. Mặc dù vậy, một số khái niệm nâng cao vẫn hiện diện
Jackie

Câu trả lời:


121

traversegiống như fmap, ngoại trừ việc nó cũng cho phép bạn chạy các hiệu ứng trong khi bạn đang xây dựng lại cấu trúc dữ liệu.

Hãy xem ví dụ từ Data.Traversabletài liệu.

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

Các Functorthể hiện của Treesẽ là:

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)

Nó xây dựng lại toàn bộ cây, áp dụng fcho mọi giá trị.

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

Các Traversableví dụ là gần như giống nhau, ngoại trừ các nhà thầu được mời gọi theo kiểu applicative. Điều này có nghĩa là chúng ta có thể có (tác dụng phụ) trong khi xây dựng lại cây. Ứng dụng gần giống như đơn nguyên, ngoại trừ hiệu ứng không thể phụ thuộc vào kết quả trước đó. Trong ví dụ này, điều đó có nghĩa là bạn không thể làm gì đó khác với nhánh phải của một nút tùy thuộc vào kết quả của việc xây dựng lại nhánh trái chẳng hạn.

Vì lý do lịch sử, Traversablelớp này cũng chứa một phiên bản đơn nguyên của traversetên gọi mapM. Đối với tất cả các ý định và mục đích mapMđều giống nhau traverse- nó tồn tại như một phương thức riêng biệt vì Applicativechỉ sau này mới trở thành một lớp cha của Monad.

Nếu bạn thực hiện điều này bằng một ngôn ngữ không tinh khiết, fmapsẽ giống như traverse, vì không có cách nào để ngăn chặn các tác dụng phụ. Bạn không thể triển khai nó như một vòng lặp, vì bạn phải duyệt đệ quy cấu trúc dữ liệu của mình. Đây là một ví dụ nhỏ về cách tôi làm điều đó trong Javascript:

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

Tuy nhiên, việc triển khai nó như vậy sẽ giới hạn bạn ở những hiệu ứng mà ngôn ngữ cho phép. Nếu bạn muốn thuyết không xác định (ví dụ trong danh sách các mô hình Ứng dụng) và ngôn ngữ của bạn không tích hợp sẵn nó, thì bạn đã không gặp may.


11
Thuật ngữ 'hiệu ứng' có nghĩa là gì?
missfaktor

24
@missingfaktor: Nó có nghĩa là thông tin cấu trúc của a Functor, phần không phải là tham số. Giá trị trạng thái trong State, lỗi trong MaybeEither, số phần tử trong []và tất nhiên là các tác dụng phụ bên ngoài tùy ý trong IO. Tôi không quan tâm đến nó như một thuật ngữ chung chung (giống như các Monoidhàm sử dụng "trống" và "nối thêm", khái niệm này chung chung hơn thuật ngữ gợi ý lúc đầu) nhưng nó khá phổ biến và phục vụ mục đích tốt.
CA McCann

@CA McCann: Hiểu rồi. Cảm ơn bạn đã trả lời!
missfaktor

1
"Tôi khá chắc rằng bạn không nên làm điều này [...]." Chắc chắn là không - nó sẽ khó chịu như việc làm cho ảnh hưởng của việc apphụ thuộc vào kết quả trước đó. Tôi đã sửa lại nhận xét đó cho phù hợp.
duplode

2
"Ứng dụng gần giống như đơn nguyên, ngoại trừ hiệu ứng không thể phụ thuộc vào kết quả trước đó." ... rất nhiều thứ được nhấn vào vị trí cho tôi với dòng này, cảm ơn!
agam

58

traversebiến những thứ bên trong a Traversablethành một Traversabletrong những thứ "bên trong" một Applicative, được cung cấp cho một chức năng tạo nên Applicativethứ bên ngoài.

Hãy sử dụng Maybeas Applicativevà liệt kê như Traversable. Đầu tiên chúng ta cần hàm chuyển đổi:

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

Vì vậy, nếu một số chẵn, chúng ta nhận được một nửa của nó (bên trong a Just), còn lại chúng ta nhận được Nothing. Nếu mọi thứ diễn ra "tốt", nó trông như thế này:

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

Nhưng...

traverse half [1..10]
-- Nothing

Lý do là <*>hàm được sử dụng để xây dựng kết quả, và khi một trong các đối số là Nothing, chúng ta sẽ lấy Nothinglại.

Một vi dụ khac:

rep x = replicate x x

Hàm này tạo ra một danh sách độ dài xvới nội dung x, ví dụ: rep 3= [3,3,3]. Kết quả là traverse rep [1..3]gì?

Chúng tôi nhận được kết quả một phần [1], [2,2][3,3,3]sử dụng rep. Bây giờ ngữ nghĩa của danh sách như Applicativeslà "lấy tất cả các kết hợp", ví dụ như (+) <$> [10,20] <*> [3,4][13,14,23,24].

"Tất cả các kết hợp" của [1][2,2]là hai lần [1,2]. Tất cả các kết hợp của hai lần [1,2][3,3,3]là sáu lần [1,2,3]. Vì vậy chúng tôi có:

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

1
Kết quả cuối cùng của bạn nhắc nhở tôi về điều này .
ômomg

3
@missingno: Vâng, họ đã bỏ lỡfac n = length $ traverse rep [1..n]
Landei

1
Trên thực tế, nó có trong "Danh sách mã hóa-lập trình viên" (nhưng sử dụng danh sách hiểu). Trang web đó là toàn diện :)
hugomg

1
@missingno: Hm, nó không hoàn toàn giống nhau ... cả hai đều dựa trên hành vi sản phẩm Descartes của đơn nguyên danh sách, nhưng trang web chỉ sử dụng hai sản phẩm cùng một lúc, vì vậy nó giống như làm liftA2 (,)hơn là sử dụng biểu mẫu chung chung traverse.
CA McCann

41

Tôi nghĩ nó dễ hiểu nhất về mặt sequenceA, traversecó thể được định nghĩa như sau.

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

sequenceA sắp xếp chuỗi các phần tử của một cấu trúc từ trái sang phải với nhau, trả về một cấu trúc có cùng hình dạng chứa các kết quả.

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

Bạn cũng có thể sequenceAcoi như đảo ngược thứ tự của hai chức năng, ví dụ: chuyển từ danh sách các hành động thành một hành động trả về một danh sách kết quả.

Vì vậy, traverselấy một số cấu trúc và áp dụng fđể chuyển đổi mọi phần tử trong cấu trúc thành một số ứng dụng, sau đó nó sắp xếp các hiệu ứng của các ứng dụng đó từ trái sang phải, trả về một cấu trúc có cùng hình dạng chứa các kết quả.

Bạn cũng có thể so sánh nó với để Foldablexác định chức năng liên quan traverse_.

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

Vì vậy, bạn có thể thấy rằng sự khác biệt chính giữa FoldableTraversablelà cái sau cho phép bạn bảo toàn hình dạng của cấu trúc, trong khi cái trước yêu cầu bạn gấp kết quả thành một số giá trị khác.


Một ví dụ đơn giản về cách sử dụng của nó là sử dụng danh sách làm cấu trúc có thể duyệt và IOlàm ứng dụng:

λ> 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"]

Mặc dù ví dụ này khá thú vị, nhưng mọi thứ trở nên thú vị hơn khi traverseđược sử dụng trên các loại thùng chứa khác hoặc sử dụng các ứng dụng khác.


Vậy traverse đơn giản là một dạng tổng quát hơn của mapM? Trong thực tế, sequenceA . fmapdanh sách tương đương với sequence . mapnó phải không?
Raskell

Ý bạn là gì khi 'trình tự các tác dụng phụ'? 'Tác dụng phụ' trong câu trả lời của bạn là gì - Tôi chỉ nghĩ rằng tác dụng phụ chỉ có thể xảy ra ở các monads. Trân trọng.
Marek

1
@Marek "Tôi chỉ nghĩ rằng các tác dụng phụ chỉ có thể xảy ra ở các đơn nguyên" - Kết nối lỏng lẻo hơn thế nhiều: (1) IO Loại có thể được sử dụng để diễn đạt các tác dụng phụ; (2) IOtình cờ là một đơn nguyên, hóa ra rất thuận tiện. Các đơn nguyên về cơ bản không liên quan đến các tác dụng phụ. Cũng cần lưu ý rằng có một nghĩa của "hiệu ứng" rộng hơn "tác dụng phụ" theo nghĩa thông thường - một nghĩa bao gồm các phép tính thuần túy. Về điểm cuối cùng này, hãy xem thêm “Hiệu quả” nghĩa là gì .
duplode

(Nhân tiện, @hammar, tôi có quyền thay đổi "tác dụng phụ" thành "hiệu ứng" trong câu trả lời này do những lý do được nêu trong nhận xét ở trên.)
duplode

17

Nó giống như vậy fmap, ngoại trừ việc bạn có thể chạy các hiệu ứng bên trong hàm ánh xạ, hàm này cũng thay đổi kiểu kết quả.

Hãy tưởng tượng một danh sách các số nguyên đại diện cho ID người dùng trong một cơ sở dữ liệu: [1, 2, 3]. Nếu bạn muốn fmapcác ID người dùng này thành tên người dùng, bạn không thể sử dụng truyền thống fmap, bởi vì bên trong hàm, bạn cần truy cập cơ sở dữ liệu để đọc tên người dùng (yêu cầu hiệu ứng - trong trường hợp này là sử dụng IOđơn nguyên).

Chữ ký của traverselà:

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

Với traverse, bạn có thể thực hiện các hiệu ứng, do đó, mã của bạn để ánh xạ ID người dùng với tên người dùng trông giống như:

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

Ngoài ra còn có một chức năng được gọi là mapM:

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

Mọi việc sử dụng mapMđều có thể được thay thế bằng traverse, nhưng không phải ngược lại. mapMchỉ hoạt động cho các đơn nguyên, trong khi traversechung chung hơn.

Nếu bạn chỉ muốn đạt được hiệu ứng và không trả về bất kỳ giá trị hữu ích nào, thì có traverse_mapM_các phiên bản của các hàm này, cả hai đều bỏ qua giá trị trả về từ hàm và nhanh hơn một chút.


Tôi đã chỉnh sửa câu trả lời của bạn vì những lý do tương tự khiến tôi phải chỉnh sửa hammar .
duplode

7

traverse vòng lặp. Việc triển khai nó phụ thuộc vào cấu trúc dữ liệu được duyệt. Đó có thể là một danh sách, cây, Maybe, Seq(ảnh hướng), hoặc bất cứ điều gì mà có một cách chung chung bị đi qua qua một cái gì đó giống như một cho vòng lặp hoặc hàm đệ quy. Một mảng sẽ có một vòng lặp for, một danh sách một vòng lặp while, một cây hoặc một cái gì đó đệ quy hoặc sự kết hợp của một ngăn xếp với một vòng lặp while; nhưng trong ngôn ngữ chức năng, bạn không cần các lệnh lặp rườm rà này: bạn kết hợp phần bên trong của vòng lặp (dưới dạng một hàm) với cấu trúc dữ liệu theo cách trực tiếp hơn và ít dài dòng hơn.

Với Traversabletypeclass, bạn có thể viết các thuật toán của mình độc lập và linh hoạt hơn. Nhưng kinh nghiệm của tôi cho biết, điều đó Traversablethường chỉ được sử dụng để gắn các thuật toán vào cấu trúc dữ liệu hiện có. Cũng khá hay khi không cần viết các hàm tương tự cho các kiểu dữ liệu khác nhau.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.