2012-07-04 33 views
36

Tôi đã bắt đầu cuộc chiến Grand Haskell (GHC :)) và tôi hơi bối rối với các chức năng đơn điệu và IO. Bất cứ ai có thể giải thích chỉ đơn giản là sự khác biệt giữa hai chức năng là gì?Sử dụng trả lại và không sử dụng trả lại trong danh sách đơn lẻ

f1 = do x <- [1,2] 
     [x, x+1] -- this is monad, right? 

f2 = do x <- [1,2] 
     return [x, x+1] 

Kết quả là:

*Main> f1 
[1,2,2,3] 

*Main> f2 
[[1,2],[2,3]] 
+8

Ngoài những câu trả lời xuất sắc, tôi muốn chỉ ra rằng bạn không phải nhầm lẫn 'return' trong lớp 'Monad' với từ khóa' return' trong các ngôn ngữ mệnh lệnh giống như C. Chúng hoàn toàn khác nhau, nhưng khá nhiều người dùng ngôn ngữ đó thấy 'return' và không nhận ra rằng nó không gây ra giá trị trả về, nó tạo ra một tính toán sẽ trả về giá trị đó khi được đánh giá tại một số điểm sau đó. Là một ngôn ngữ chức năng, Haskell không cần bất cứ thứ gì giống như câu lệnh 'return'. –

+2

Thật vậy, sự lựa chọn của Haskell để sử dụng từ 'return' là, imho, một sai lầm lớn. Nhưng do khả năng tương thích ngược, hiện tại không có quay trở lại! –

+4

@DanBurton: bạn luôn có thể tạo bí danh :) 'do_not_return a = return a' –

Trả lời

22

Những câu trả lời khác ở đây là chính xác, nhưng tôi tự hỏi, nếu họ không hoàn toàn những gì bạn cần ... Tôi sẽ cố gắng để giữ này là đơn giản càng tốt, chỉ cần hai điểm:


Điểm 1. return không phải là điều đặc biệt bằng ngôn ngữ Haskell. Nó không phải là một từ khóa, và nó không phải là cú pháp đường cho cái gì khác. Nó chỉ là một chức năng mà là một phần của typeclass Monad. Chữ ký của nó chỉ đơn giản là:

return :: a -> m a 

trong đó m là chúng tôi đang nói đến cái nào vào lúc đó. Nó có một giá trị "tinh khiết" và mứt nó vào monad của bạn. (! Ngẫu nhiên, có một chức năng gọi là pure đó là cơ bản là một từ đồng nghĩa với return ... tôi thích nó tốt hơn bởi vì tên của nó là rõ ràng hơn) Dù sao, nếu m là danh sách đơn nguyên, sau đó return có kiểu này:

return :: a -> [a] 

Nếu nó giúp, bạn có thể nghĩ về loại đồng nghĩa type List a = [a], có thể làm cho nó rõ ràng hơn một chút rằng List là thứ chúng ta đang thay thế cho m. Dù sao, nếu bạn đang đi để thực hiện return mình, cách hợp lý duy nhất mà bạn muốn thực hiện nó là bằng cách tham gia một số giá trị (của bất cứ loại a) và gắn bó nó trong một danh sách bằng cách riêng của mình:

return a = [a] 

Vì vậy, tôi có thể nói return 1 trong danh sách đơn lẻ và tôi sẽ nhận được [1]. Tôi cũng có thể nói return [1, 2, 3] và tôi sẽ nhận được [[1, 2, 3]].


Điểm 2.IO là một đơn nguyên, nhưng không phải tất cả các mon đều là IO. Nhiều hướng dẫn Haskell dường như liên kết hai chủ đề chủ yếu vì lý do lịch sử (tình cờ, cùng một lý do gây nhầm lẫn lịch sử dẫn đến return quá kém tên). Có vẻ như bạn có thể có một số sự nhầm lẫn (dễ hiểu) xung quanh điều đó.

Trong mã của bạn, bạn đang trong danh sách đơn lẻ vì bạn đã viết do x <- [1, 2]. Thay vào đó, nếu bạn đã viết do x <- getLine, bạn sẽ ở trong đơn vị IO (vì getLine trả lại IO String). Dù sao, bạn đang ở trong danh sách đơn nguyên, vì vậy bạn sẽ có được định nghĩa của danh sách là return được mô tả ở trên. Bạn cũng có thể định nghĩa danh sách của >>=, mà chỉ là (một phiên bản lật của) concatMap, định nghĩa là:

concatMap :: (a -> [b]) -> [a] -> [b] 
concatMap f xs = concat (map f xs) 

Các khác được đăng câu trả lời khá nhiều có nó bao phủ từ đây :) Tôi biết tôi không trả lời của bạn câu hỏi trực tiếp, nhưng tôi hy vọng hai điểm này thay vì giải quyết những điều cơ bản mà bạn có thể đã thấy khó hiểu.

24

Thật dễ dàng để xem khi nào lại viết code với bindtrở:

[1,2] >>= (\x->       [x,x+1]) === concatMap (\x-> [ x,x+1 ]) [1,2] 

[1,2] >>= (\x-> return [x,x+1]) === concatMap (\x-> [ [x,x+1] ]) [1,2] 

mã đầu tiên của bạn là tương đương với gọi join trên kết quả của lần thứ hai, loại bỏ một "lớp" đơn lẻ được giới thiệu bởi return :: a -> m a, conflating các "danh sách" của đơn nguyên được sử dụng, với "danh sách" giá trị của bạn. Nếu bạn được trả lại một cặp, chẳng hạn, nó sẽ không có được nhiều ý nghĩa để bỏ qua return:

         -- WRONG: type mismatch 
[1,2] >>= (\x->        (x,x+1)) === concatMap (\x-> ( x,x+1 )) [1,2] 
            -- OK: 
[1,2] >>= (\x-> return (x,x+1)) === concatMap (\x-> [ (x,x+1) ]) [1,2] 

Hoặc chúng ta có thể sử dụng một join/fmap lại ghi:

ma >>= famb === join (fmap famb ma) -- famb :: a -> m b, m ~ [] 

join (fmap (\x->  [x,x+1]) [1,2]) = concat [ [ x,x+1 ] | x<-[1,2]] 
join (fmap (\x->  (x,x+1)) [1,2]) = concat [ ( x,x+1 ) | x<-[1,2]] -- WRONG 
join (fmap (\x-> return [x,x+1]) [1,2]) = concat [ [ [x,x+1] ] | x<-[1,2]] 

                 = [y | x<-[1,2], y<-[ x,x+1 ]] 
              {- WRONG -} = [y | x<-[1,2], y<-(x,x+1)] 
                 = [y | x<-[1,2], y<-[[x,x+1]]] 
+0

mã đầu tiên, theo luật Monad, tương đương với 'do {x <- [1,2]; r <- [x, x + 1]; return r} 'mà, với Monad Comprehensions, được viết '[r | x <- [1,2], r <- [x, x + 1]] '; thứ hai là '[[x, x + 1] | x <- [1,2]] '. vì vậy, 'f1 = lật ($) <$> [1,2] <*> [id, (1 +)]' và 'f2 = (\ x -> [x, x + 1]) <$> [1,2]'.- với tuple như trong câu trả lời chúng tôi nhận được 'lật ($) <$> [1,2] <*> (id, (1 +))' (* sai *) và '(\ x -> (x, x + 1)) <$> [1,2] '(* OK *). –

13

danh sách loại ([]) là một đơn nguyên, vâng.

Bây giờ, hãy nhớ những gì return thực hiện. Điều này rất dễ nhìn thấy từ chữ ký loại của nó: return :: Monad m => a -> m a. Hãy thay thế loại danh sách trong: return :: a -> [a]. Vì vậy, hàm này lấy một số giá trị và trả về chỉ một danh sách giá trị đó. Nó tương đương với \ x -> [x].

Vì vậy, trong mẫu mã đầu tiên, bạn có một danh sách ở cuối cùng: [x, x+1]. Trong mẫu thứ hai, bạn có một danh sách lồng nhau: một danh sách xuất phát từ [x, x + 1]một danh sách khác xuất phát từ return. Dòng return [x, x + 1] có thể được viết lại thành [[x, x + 1]] trong trường hợp này.

Cuối cùng, kết quả là danh sách tất cả các kết quả có thể có. Tức là, chúng tôi nối kết quả của x1 và kết quả của x2 (nhờ đường dây x <- [1,2]). Vì vậy, trong trường hợp đầu tiên, chúng tôi nối hai danh sách; trong trường hợp thứ hai, chúng tôi nối hai danh sách của danh sách, vì thêm return đã bao gồm kết quả trong danh sách bổ sung.

8

Desugaring cú pháp do để tương đương với

f1 = [1,2] >>= \x -> [x, x+1] 
f2 = [1,2] >>= \x -> return [x, x+1] 

Bây giờ, >>= xuất phát từ lớp Monad,

class Monad m where 
    (>>=) :: m a -> (a -> m b) -> m b 
    return :: a -> m a 

và LHS của >>= trong cả f1f2[a] (nơi a có được mặc định là Integer), vì vậy, chúng tôi thực sự xem xét

instance Monad [] where 
    (>>=) :: [a] -> (a -> [b]) -> [b] 
    ... 

này tuân theo cùng monad laws như, nhưng là một đơn nguyên khác nhau từ,

instance Monad IO where ... 

>>= cho monads khác, do đó, không chỉ là một cách mù quáng áp dụng những gì bạn biết về một đến khác, đuợc? :)

instance Monad [] được định nghĩa thusly trong GHC

instance Monad [] where 
    m >>= k = foldr ((++) . k) [] m 
    return x = [x] 
    ... 

nhưng nó có lẽ là dễ dàng hơn để hiểu [] 's >>= như

instance Monad [] where 
    m >>= k = concatMap k m 

Nếu bạn thực hiện việc này và áp dụng nó với bản gốc, bạn nhận được

f1 = concatMap (\x -> [x, x+1]) [1,2] 
f2 = concatMap (\x -> [[x, x+1]]) [1,2] 

và rõ ràng lý do tại sao Giá trị e là f1f2 là giá trị của chúng.

42

Để xem lý do tại sao bạn nhận được các câu trả lời cụ thể phát sinh, các giải thích rõ ràng rất hữu ích. Hãy để tôi bổ sung cho họ với một chút lời khuyên chung về phát triển nhận thức của mã Haskell.

hệ thống kiểu Haskell làm cho không có sự phân biệt giữa hai tách mục đích "đạo đức":

  • [x] loại giá trị đó là danh sách với các yếu tố rút ra từ x
  • [x] loại tính của các yếu tố của x cho phép lựa chọn ưu tiên

Thực tế là hai khái niệm này có cùng biểu diễn không có nghĩa là chúng đóng vai trò giống nhau. Trong f1, [x, x+1] đang đóng vai trò tính toán, do đó, khả năng mà nó tạo được sáp nhập vào lựa chọn được tạo ra bởi toàn bộ tính toán: đó là những gì mà >>= của danh sách đơn vị thực hiện. Tuy nhiên, trong f2, [x, x+1] đóng vai trò giá trị, do đó toàn bộ phép tính tạo ra một sự lựa chọn ưu tiên giữa hai giá trị (xảy ra là giá trị danh sách).

Haskell không sử dụng các loại để tạo sự khác biệt này [và bạn có thể đoán trước bây giờ tôi nghĩ điều đó, nhưng đó là một câu chuyện khác]. Thay vào đó, nó sử dụng cú pháp.Vì vậy, bạn cần phải đào tạo đầu của bạn để cảm nhận giá trị và vai trò tính toán khi bạn đọc mã. Ký hiệu do là cú pháp đặc biệt để xây dựng các tính toán . Những gì diễn ra bên trong do được xây dựng từ các mẫu kit sau:

jigsaw pieces for computations

Ba mảnh màu xanh làm do -computations. Tôi đã đánh dấu các lỗ tính toán màu xanh lam và các lỗ giá trị màu đỏ. Điều này không có nghĩa là một cú pháp hoàn chỉnh, chỉ là một hướng dẫn về cách nhận biết các đoạn mã trong đầu bạn. Thật vậy, bạn có thể viết bất kỳ biểu thức cũ ở những nơi màu xanh với điều kiện nó có loại đơn điệu phù hợp, và tính toán được tạo ra sẽ được sáp nhập vào tính toán tổng thể bằng cách sử dụng >>= khi cần thiết. Trong ví dụ f1 của bạn, danh sách của bạn ở một nơi màu xanh và được coi là lựa chọn ưu tiên.

Tương tự, bạn có thể viết biểu thức ở những vị trí màu đỏ có thể có các loại đơn sắc (như danh sách trong trường hợp này), nhưng chúng sẽ được coi là giá trị giống nhau. Đó là những gì xảy ra trong f2: như vậy, các dấu ngoặc bên ngoài của kết quả là màu xanh, nhưng các dấu ngoặc trong có màu đỏ.

Đào tạo não của bạn để tách giá trị/tính toán khi bạn đọc mã, để bạn biết bản năng những phần nào của văn bản đang thực hiện công việc nào. Khi bạn đã lập trình lại đầu của mình, sự khác biệt giữa f1f2 sẽ có vẻ hoàn toàn bình thường!

+1

Re "Haskell không sử dụng các loại để tạo ra sự khác biệt này [và bây giờ bạn có thể đoán được rằng tôi nghĩ nó nên ...": Chắc chắn một phần quan điểm của khái niệm đơn nguyên là những kiểu này chúng ta đã nói dối , có thể, vv, xảy ra để cũng được monads bởi vì họ hỗ trợ các hoạt động đúng, và do đó có thể được sử dụng như cả monads và các loại container. Bạn có ủng hộ rằng chúng tôi không lập danh sách một đơn vị, và thay vào đó phát minh ra một loại mới giống như danh sách và làm cho một đơn nguyên, để chúng tôi không thể kết hợp hoạt động danh sách bình thường và hoạt động đơn lẻ? – Ben

+0

@ Có, đó có vẻ là những gì anh ấy đang ủng hộ. Chúng ta làm 'newtype Ziplist a ... ', tại sao sự bất đối xứng? Có một số 'bind' có thể thực thi bên cạnh' concatMap' đơn giản, nói một cái xen kẽ giữa các dòng, '(\' bind_i \ 'f) = foldr (++ /) []. fmap f' trong đó '(x: xs) ++/ys = x: ys ++/xs'. Chúng ta cũng phải 'newtype', vậy tại sao sự bất đối xứng? Điều đó dường như là đối số. Tôi nghi ngờ ở một mức độ sâu hơn nó là một sự khác biệt giữa danh sách như là loại bê tông và là một loại trừu tượng. Chúng tôi có thể thực hiện danh sách trên cây nhị phân cân bằng và sẽ phải sao chép mã định nghĩa đơn nguyên. –

+0

@WillNess Đúng là có tối đa một "phiên bản" của danh sách đơn lẻ có thể được sử dụng trực tiếp trên danh sách gốc, do đó có sự bất đối xứng giữa trường hợp "mặc định" đó và các phiên bản bổ sung mới. Chúng ta có thể tránh điều đó bằng cách * luôn luôn * tạo ra một kiểu mới, nhưng điều này không liên quan gì đến monads, vì vậy chúng ta có lẽ nên làm điều đó cho tất cả các loại lớp, điều này nghe có vẻ đau đớn với tôi. – Ben

1

sự hiểu biết của tôi là làm

trở lại [1,2] khi trong danh sách đơn nguyên là giống như làm

func :: Maybe (Maybe Int) 
func = return $ Just 1 

đó là lý do tại sao bạn kết thúc với danh sách bọc, như [] là chỉ cú pháp đường phải không?

Khi thực sự anh muốn làm

func :: Maybe Int 
func = return 5 

hoặc

func = Just 5 

của nó một chút dễ dàng hơn để xem những gì đang xảy ra với các lẽ đơn nguyên Tôi nghĩ.

Vì vậy, khi bạn làm

return [1,2] 

Bạn đang làm giống như

[ [1,2] ] 
+0

'return Chỉ 1' phân tích cú pháp như' (return Just) 1' mà không hoạt động: ( – ephemient

+0

Ah cảm ơn rằng lẽ ra phải là, return (Chỉ 1) – FEiN

Các vấn đề liên quan