Tại sao chúng ta cần các đơn nguyên?


366

Theo ý kiến ​​khiêm tốn của tôi, câu trả lời cho câu hỏi nổi tiếng "Thế nào là một đơn nguyên?" , đặc biệt là những người được bình chọn nhiều nhất, cố gắng giải thích thế nào là một đơn nguyên mà không giải thích rõ ràng tại sao các đơn vị thực sự cần thiết . Họ có thể được giải thích là giải pháp cho một vấn đề?




4
Những nghiên cứu bạn đã thực hiện? Bạn đã nhìn ở đâu? Những tài nguyên bạn đã tìm thấy? Chúng tôi hy vọng bạn thực hiện một lượng nghiên cứu đáng kể trước khi hỏi và cho chúng tôi biết câu hỏi bạn đã thực hiện nghiên cứu nào . Có nhiều tài nguyên cố gắng giải thích động lực cho tài nguyên - nếu bạn chưa tìm thấy chút nào, bạn có thể cần nghiên cứu thêm một chút. Nếu bạn đã tìm thấy một số nhưng họ không giúp bạn, nó sẽ làm cho câu hỏi này tốt hơn nếu bạn giải thích những gì bạn đã tìm thấy và tại sao cụ thể họ không làm việc cho bạn.
DW

8
Điều này chắc chắn là phù hợp hơn cho Lập trình viên .StackExchange và không phù hợp với StackOverflow. Tôi sẽ bỏ phiếu để di chuyển nếu tôi có thể, nhưng tôi không thể. = (
jpmc26

3
@ jpmc26 Rất có thể nó sẽ bị đóng ở đó là "chủ yếu dựa trên quan điểm"; ít nhất đây là một cơ hội (như được thể hiện bởi số lượng người ủng hộ khổng lồ, nhanh chóng mở cửa trở lại vào ngày hôm qua và không có thêm phiếu bầu nào nữa)
Izkata

Câu trả lời:


580

Tại sao chúng ta cần các đơn nguyên?

  1. Chúng tôi muốn lập trình chỉ sử dụng các chức năng . ("Lập trình chức năng (FP)" sau khi tất cả).
  2. Sau đó, chúng tôi có một vấn đề lớn đầu tiên. Đây là một chương trình:

    f(x) = 2 * x

    g(x,y) = x / y

    Làm thế nào chúng ta có thể nói những gì sẽ được thực hiện đầu tiên ? Làm thế nào chúng ta có thể hình thành một chuỗi các hàm theo thứ tự (tức là một chương trình ) bằng cách sử dụng không nhiều hơn các hàm ?

    Giải pháp: soạn các hàm . Nếu bạn muốn đầu tiên gvà sau đó f, chỉ cần viết f(g(x,y)). Theo cách này, "chương trình" cũng là một chức năng : main = f(g(x,y)). Được rồi nhưng ...

  3. Nhiều vấn đề hơn: một số chức năng có thể không thành công (nghĩa là g(2,0)chia cho 0). Chúng tôi không có "ngoại lệ" trong FP (ngoại lệ không phải là chức năng). Làm thế nào để chúng ta giải quyết nó?

    Giải pháp: Chúng ta hãy cho phép các hàm trả về hai loại : thay vì có g : Real,Real -> Real(hàm từ hai thực thành thực), hãy cho phép g : Real,Real -> Real | Nothing(hàm từ hai thực thành (thực hoặc không có gì)).

  4. Nhưng các hàm nên (đơn giản hơn) chỉ trả về một thứ .

    Giải pháp: chúng ta hãy tạo ra một loại dữ liệu mới được trả về, một " kiểu đấm bốc " có thể là thật hoặc đơn giản là không có gì. Do đó, chúng ta có thể có g : Real,Real -> Maybe Real. Được rồi nhưng ...

  5. Điều gì xảy ra bây giờ f(g(x,y))? fchưa sẵn sàng để tiêu thụ a Maybe Real. Và, chúng tôi không muốn thay đổi mọi chức năng mà chúng tôi có thể kết nối gđể tiêu thụ a Maybe Real.

    Giải pháp: chúng ta hãy có một chức năng đặc biệt để "kết nối" / "soạn thảo" / "liên kết" các chức năng . Bằng cách đó, chúng ta có thể, đằng sau hậu trường, điều chỉnh đầu ra của một chức năng để cung cấp chức năng sau.

    Trong trường hợp của chúng tôi: g >>= f(kết nối / soạn gthành f). Chúng tôi muốn >>=nhận gđầu ra, kiểm tra nó và, trong trường hợp đó Nothingchỉ là không gọi fvà trả lại Nothing; hoặc ngược lại, giải nén hộp Realvà cho fnó ăn . (Thuật toán này chỉ là việc thực hiện >>=cho Maybeloại). Cũng lưu ý rằng chỉ >>=được viết một lần cho mỗi "loại quyền anh" (hộp khác nhau, thuật toán thích ứng khác nhau).

  6. Nhiều vấn đề khác phát sinh có thể được giải quyết bằng cách sử dụng cùng một mẫu này: 1. Sử dụng "hộp" để mã hóa / lưu trữ các ý nghĩa / giá trị khác nhau và có các hàm như gtrả về các "giá trị được đóng hộp" đó. 2. Có một nhà soạn nhạc / trình liên kết g >>= fđể giúp kết nối gđầu ra fcủa đầu vào với đầu vào, vì vậy chúng tôi không phải thay đổi gì fcả.

  7. Các vấn đề đáng chú ý có thể được giải quyết bằng kỹ thuật này là:

    • có trạng thái toàn cầu mà mọi chức năng trong chuỗi chức năng ("chương trình") có thể chia sẻ: giải pháp StateMonad.

    • Chúng tôi không thích "hàm không tinh khiết": các hàm mang lại đầu ra khác nhau cho cùng một đầu vào. Do đó, hãy đánh dấu các hàm đó, làm cho chúng trả về giá trị được gắn thẻ / đóng hộp: IOđơn nguyên.

Hạnh phúc trọn vẹn!


64
@Carl Hãy viết một câu trả lời tốt hơn để soi sáng cho chúng tôi
XrXr

15
@Carl Tôi nghĩ rằng rõ ràng trong câu trả lời rằng có nhiều vấn đề được hưởng lợi từ mẫu này (điểm 6) và IOđơn nguyên đó chỉ là một vấn đề nữa trong danh sách IO(điểm 7). Mặt khác, IOchỉ xuất hiện một lần và cuối cùng, vì vậy, đừng hiểu "phần lớn thời gian của bạn nói về ... về IO".
cibercitizen1

4
Những quan niệm sai lầm lớn về các đơn nguyên: các đơn vị về nhà nước; đơn nguyên về xử lý ngoại lệ; không có cách nào để thực hiện IO trong FPL thuần túy mà không có đơn nguyên, đơn nguyên không rõ ràng (tương phản là Either). Phần lớn câu trả lời là về "Tại sao chúng ta cần functor?".
vlastachu

4
"6. 2. Có một nhà soạn nhạc / trình liên kết g >>= fđể giúp kết nối gđầu ra fcủa đầu vào với đầu vào, vì vậy chúng tôi không phải thay đổi bất kỳ thứ fgì." Điều này không đúng chút nào. Trước, trong f(g(x,y)), fcó thể sản xuất bất cứ điều gì. Nó có thể được f:: Real -> String. Với "thành phần đơn âm", nó phải được thay đổi để sản xuất Maybe String, nếu không các loại sẽ không phù hợp. Hơn nữa, >>=bản thân nó không phù hợp !! Đó là >=>thành phần này, không phải >>=. Xem cuộc thảo luận với dfeuer dưới câu trả lời của Carl.
Will Ness

3
Câu trả lời của bạn là đúng theo nghĩa các đơn vị IMO thực sự được mô tả tốt nhất là về thành phần / tính chất của "các chức năng" (mũi tên Kleisli thực sự), nhưng các chi tiết chính xác về loại đi đâu là thứ khiến chúng trở thành "đơn nguyên". bạn có thể nối các hộp trong tất cả các cách cư xử (như Functor, v.v.). Cách cụ thể này để nối chúng lại với nhau là những gì định nghĩa "đơn nguyên".
Will Ness

219

Câu trả lời là, tất nhiên, "Chúng tôi không" . Như với tất cả các khái niệm trừu tượng, nó không cần thiết.

Haskell không cần một bản tóm tắt đơn nguyên. Không cần thiết để thực hiện IO bằng ngôn ngữ thuần túy. Các IOloại chăm sóc đó chỉ tốt thôi. Các desugaring monadic hiện có của dokhối có thể được thay thế bằng desugaring đến bindIO, returnIOfailIOtheo quy định tại các GHC.Basemô-đun. (Đây không phải là một mô-đun tài liệu về hackage, vì vậy tôi sẽ phải chỉ vào nguồn của nó để làm tài liệu.) Vì vậy, không cần sự trừu tượng hóa đơn nguyên.

Vì vậy, nếu nó không cần thiết, tại sao nó tồn tại? Bởi vì nó đã được tìm thấy rằng nhiều mô hình tính toán hình thành các cấu trúc đơn nguyên. Sự trừu tượng hóa của một cấu trúc cho phép viết mã hoạt động trên tất cả các phiên bản của cấu trúc đó. Nói một cách chính xác hơn - tái sử dụng mã.

Trong các ngôn ngữ chức năng, công cụ mạnh nhất được tìm thấy để tái sử dụng mã là thành phần của các hàm. (.) :: (b -> c) -> (a -> b) -> (a -> c)Toán tử cũ tốt là cực kỳ mạnh mẽ. Nó giúp bạn dễ dàng viết các hàm nhỏ và dán chúng lại với nhau với chi phí cú pháp hoặc ngữ nghĩa tối thiểu.

Nhưng có những trường hợp khi các loại không hoạt động hoàn toàn đúng. Bạn làm gì khi bạn có foo :: (b -> Maybe c)bar :: (a -> Maybe b)? foo . barkhông đánh máy, bởi vì bMaybe bkhông cùng loại.

Nhưng ... nó gần như đúng. Bạn chỉ muốn một chút chậm trễ. Bạn muốn có thể đối xử Maybe bnhư thể về cơ bản b. Mặc dù vậy, đó là một ý tưởng tồi khi chỉ thẳng thắn coi chúng là cùng loại. Điều đó ít nhiều giống với con trỏ null, mà Tony Hoare nổi tiếng gọi là sai lầm tỷ đô . Vì vậy, nếu bạn không thể coi chúng là cùng loại, có lẽ bạn có thể tìm cách mở rộng cơ chế sáng tác (.)cung cấp.

Trong trường hợp đó, điều quan trọng là thực sự kiểm tra lý thuyết cơ bản (.). May mắn thay, ai đó đã làm điều này cho chúng tôi. Nó chỉ ra rằng sự kết hợp (.)idtạo thành một cấu trúc toán học được gọi là một thể loại . Nhưng có những cách khác để hình thành thể loại. Ví dụ, một loại Kleisli, cho phép các đối tượng được sáng tác được tăng thêm một chút. Một danh mục Kleisli Maybesẽ bao gồm (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)id :: a -> Maybe a. Đó là, các đối tượng trong danh mục tăng thêm (->)với a Maybe, vì vậy (a -> b)trở thành (a -> Maybe b).

Và đột nhiên, chúng tôi đã mở rộng sức mạnh của bố cục sang những thứ mà (.)hoạt động truyền thống không hoạt động. Đây là một nguồn sức mạnh trừu tượng mới. Thể loại Kleisli làm việc với nhiều loại hơn chỉ Maybe. Họ làm việc với mọi loại có thể lắp ráp một danh mục phù hợp, tuân theo luật về thể loại.

  1. Danh tính bên trái: id . f=f
  2. Đúng danh tính: f . id=f
  3. Tính kết hợp: f . (g . h)=(f . g) . h

Miễn là bạn có thể chứng minh rằng loại của bạn tuân theo ba luật đó, bạn có thể biến nó thành một loại Kleisli. Và vấn đề lớn về điều đó là gì? Chà, hóa ra các đơn nguyên giống hệt như các thể loại Kleisli. Monadcủa returngiống như Kleisli id. Monad's (>>=)không phải là giống hệt nhau để Kleisli (.), nhưng nó hóa ra là rất dễ dàng để viết từng về mặt khác. Và các luật về thể loại cũng giống như các luật đơn nguyên, khi bạn dịch chúng qua sự khác biệt giữa (>>=)(.).

Vậy tại sao phải trải qua tất cả những điều này? Tại sao có sự Monadtrừu tượng trong ngôn ngữ? Như tôi đã nói ở trên, nó cho phép tái sử dụng mã. Nó thậm chí còn cho phép tái sử dụng mã dọc theo hai chiều khác nhau.

Kích thước đầu tiên của việc tái sử dụng mã đến trực tiếp từ sự hiện diện của sự trừu tượng hóa. Bạn có thể viết mã hoạt động trên tất cả các trường hợp trừu tượng. Có toàn bộ gói vòng lặp bao gồm các vòng lặp hoạt động với bất kỳ trường hợp nào Monad.

Chiều thứ hai là gián tiếp, nhưng nó xuất phát từ sự tồn tại của thành phần. Khi bố cục dễ dàng, việc viết mã thành các phần nhỏ, có thể tái sử dụng là điều tự nhiên. Đây là cùng một cách có (.)toán tử cho các chức năng khuyến khích viết các chức năng nhỏ, có thể tái sử dụng.

Vậy tại sao sự trừu tượng tồn tại? Bởi vì nó được chứng minh là một công cụ cho phép nhiều thành phần hơn trong mã, dẫn đến việc tạo mã có thể sử dụng lại và khuyến khích tạo mã có thể tái sử dụng nhiều hơn. Tái sử dụng mã là một trong những hạt thánh của lập trình. Sự trừu tượng của đơn nguyên tồn tại bởi vì nó di chuyển chúng ta một chút về phía chén thánh đó.


2
Bạn có thể giải thích mối quan hệ giữa các loại nói chung và các loại Kleisli? Ba luật bạn mô tả giữ trong bất kỳ thể loại.
dfeuer

1
@dfeuer ơi. Để đặt nó trong mã , newtype Kleisli m a b = Kleisli (a -> m b). Các danh mục Kleisli là các hàm trong đó kiểu trả về phân loại ( btrong trường hợp này) là đối số cho hàm tạo kiểu m. Iff Kleisli mtạo thành một thể loại, mlà một Monad.
Carl

1
Một loại trả lại chính xác là gì? Kleisli mdường như tạo thành một thể loại có các đối tượng là các loại Haskell và sao cho các mũi tên từ ađến blà các hàm từ ađến m b, với id = return(.) = (<=<). Điều đó có đúng không, hay tôi đang trộn lẫn các cấp độ khác nhau của một thứ hoặc một cái gì đó?
dfeuer

1
@dfeuer Đúng vậy. Các đối tượng là tất cả các loại, và các hình thái là giữa các loại ab, nhưng chúng không phải là các chức năng đơn giản. Chúng được trang trí thêm một mgiá trị trả về của hàm.
Carl

1
Là thuật ngữ lý thuyết thể loại thực sự cần thiết? Có thể, Haskell sẽ dễ dàng hơn nếu bạn biến các loại thành hình ảnh trong đó loại sẽ là DNA để vẽ hình ảnh (loại phụ thuộc mặc dù *), và sau đó bạn sử dụng hình ảnh để viết chương trình của mình với tên là các ký tự ruby ​​nhỏ phía trên biểu tượng.
aoeu256

24

Benjamin Pierce nói trong TAPL

Một hệ thống loại có thể được coi là tính toán một loại xấp xỉ tĩnh đối với các hành vi trong thời gian chạy của các điều khoản trong một chương trình.

Đó là lý do tại sao một ngôn ngữ được trang bị một hệ thống loại mạnh mẽ lại biểu cảm rõ ràng hơn là một ngôn ngữ được đánh máy kém. Bạn có thể nghĩ về các đơn nguyên theo cùng một cách.

Là điểm @Carl và sigfpe , bạn có thể trang bị một kiểu dữ liệu với tất cả các hoạt động bạn muốn mà không cần dùng đến các đơn nguyên, kiểu chữ hoặc bất kỳ nội dung trừu tượng nào khác. Tuy nhiên, các đơn vị cho phép bạn không chỉ viết mã có thể tái sử dụng mà còn để trừu tượng hóa tất cả các chi tiết dư thừa.

Ví dụ: giả sử chúng tôi muốn lọc một danh sách. Cách đơn giản nhất là sử dụng filterhàm : filter (> 3) [1..10], bằng [4,5,6,7,8,9,10].

Một phiên bản phức tạp hơn một chút filter, cũng chuyển một bộ tích lũy từ trái sang phải, là

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]

Để có được tất cả i, như vậy i <= 10, sum [1..i] > 4, sum [1..i] < 25, chúng ta có thể viết

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

mà bằng [3,4,5,6].

Hoặc chúng ta có thể xác định lại nubhàm, loại bỏ các phần tử trùng lặp khỏi danh sách, theo filterAccum:

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

nub' [1,2,4,5,4,3,1,8,9,4]bằng [1,2,4,5,3,8,9]. Một danh sách được thông qua như là một tích lũy ở đây. Mã này hoạt động, bởi vì nó có thể rời khỏi danh sách đơn nguyên, vì vậy toàn bộ tính toán vẫn ở dạng thuần túy ( thực tế notElemkhông sử dụng >>=, nhưng nó có thể). Tuy nhiên, không thể rời khỏi đơn vị IO một cách an toàn (nghĩa là bạn không thể thực hiện hành động IO và trả về giá trị thuần túy - giá trị sẽ luôn được gói trong đơn vị IO). Một ví dụ khác là mảng có thể thay đổi: sau khi bạn đã nhảy đơn vị ST, nơi một mảng có thể thay đổi trực tiếp, bạn không thể cập nhật mảng trong thời gian liên tục nữa. Vì vậy, chúng ta cần một bộ lọc đơn âm từ Control.Monadmô-đun:

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterMthực hiện một hành động đơn nguyên cho tất cả các yếu tố từ một danh sách, mang lại các yếu tố mà hành động đơn nguyên đó trả về True.

Một ví dụ lọc với một mảng:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

in [1,2,4,5,3,8,9]như mong đợi.

Và một phiên bản với đơn nguyên IO, yêu cầu trả về các yếu tố nào:

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

Ví dụ

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

Và như một minh họa cuối cùng, filterAccumcó thể được định nghĩa theo filterM:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

với StateTđơn nguyên, được sử dụng dưới mui xe, chỉ là một kiểu dữ liệu thông thường.

Ví dụ này minh họa, các đơn nguyên không chỉ cho phép bạn tóm tắt bối cảnh tính toán và viết mã có thể tái sử dụng sạch (do khả năng kết hợp của các đơn vị, như @Carl giải thích), mà còn xử lý các kiểu dữ liệu do người dùng xác định và các nguyên hàm được xây dựng đồng nhất.


1
Câu trả lời này giải thích, tại sao chúng ta cần kiểu chữ Monad. Cách tốt nhất để hiểu, tại sao chúng ta cần các đơn nguyên mà không phải thứ gì khác, là đọc về sự khác biệt giữa các đơn vị và các chức năng ứng dụng: một , hai .
dùng3237465

20

Tôi không nghĩ IOnên được coi là một đơn vị đặc biệt xuất sắc, nhưng nó chắc chắn là một trong những người đáng kinh ngạc hơn cho người mới bắt đầu, vì vậy tôi sẽ sử dụng nó để giải thích.

Ngây thơ xây dựng một hệ thống IO cho Haskell

Hệ thống IO có thể hiểu được đơn giản nhất cho một ngôn ngữ có chức năng thuần túy (và trên thực tế, một Haskell bắt đầu với) là:

main :: String -> String
main _ = "Hello World"

Với sự lười biếng, chữ ký đơn giản đó đủ để thực sự xây dựng các chương trình thiết bị đầu cuối tương tác - mặc dù rất hạn chế. Bực bội nhất là chúng tôi chỉ có thể xuất văn bản. Điều gì nếu chúng ta thêm một số khả năng đầu ra thú vị hơn?

data Output = TxtOutput String
            | Beep Frequency

main :: String -> [Output]
main _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]

dễ thương, nhưng dĩ nhiên, một đầu ra thay đổi thực tế hơn nhiều, sẽ được ghi vào một tập tin . Nhưng sau đó bạn cũng muốn một số cách để đọc từ các tập tin. Bất cứ cơ hội nào?

Vâng, khi chúng tôi thực hiện main₁chương trình của mình và chỉ cần đưa một tệp vào quy trình (sử dụng các phương tiện hệ điều hành), về cơ bản chúng tôi đã thực hiện đọc tệp. Nếu chúng ta có thể kích hoạt việc đọc tệp từ trong ngôn ngữ Haskell ...

readFile :: Filepath -> (String -> [Output]) -> [Output]

Điều này sẽ sử dụng một chương trình tương tác trên mạng String->[Output], xếp hạng, cung cấp cho nó một chuỗi thu được từ một tệp và mang lại một chương trình không tương tác chỉ đơn giản thực hiện một chuỗi đã cho.

Có một vấn đề ở đây: chúng tôi thực sự không có khái niệm khi nào tệp được đọc. Các [Output]danh sách chắc chắn mang đến cho một trật tự tốt đẹp để các kết quả đầu ra , nhưng chúng tôi không nhận được một đơn đặt hàng khi đầu vào sẽ được thực hiện.

Giải pháp: tạo các sự kiện đầu vào cũng là các mục trong danh sách những việc cần làm.

data IO = TxtOut String
         | TxtIn (String -> [Output])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [Output])
         | Beep Double

main :: String -> [IO₀]
main _ = [ FileRead "/dev/null" $ \_ ->
             [TxtOutput "Hello World"]
          ]

Ok, bây giờ bạn có thể phát hiện ra sự mất cân bằng: bạn có thể đọc một tệp và làm cho đầu ra phụ thuộc vào nó, nhưng bạn không thể sử dụng nội dung tệp để quyết định, ví dụ như cũng đọc một tệp khác. Giải pháp rõ ràng: làm cho kết quả của các sự kiện đầu vào cũng là một kiểu gì đó IO, không chỉ Output. Điều đó chắc chắn bao gồm đầu ra văn bản đơn giản, nhưng cũng cho phép đọc các tệp bổ sung, v.v.

data IO = TxtOut String
         | TxtIn (String -> [IO₁])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [IO₁])
         | Beep Double

main :: String -> [IO₁]
main _ = [ TxtIn $ \_ ->
             [TxtOut "Hello World"]
          ]

Điều đó bây giờ thực sự sẽ cho phép bạn thể hiện bất kỳ thao tác tệp nào bạn có thể muốn trong một chương trình (mặc dù có lẽ không có hiệu suất tốt), nhưng nó có phần quá phức tạp:

  • main₃mang lại một danh sách toàn bộ các hành động. Tại sao chúng ta không đơn giản sử dụng chữ ký :: IO₁, trong đó có trường hợp đặc biệt?

  • Các danh sách không thực sự cung cấp một cái nhìn tổng quan đáng tin cậy về dòng chảy chương trình nữa: hầu hết các tính toán tiếp theo sẽ chỉ được công bố và là kết quả của một số hoạt động đầu vào. Vì vậy, chúng tôi cũng có thể bỏ cấu trúc danh sách, và chỉ đơn giản là sử dụng một tên lửa và sau đó thực hiện các hoạt động đầu ra.

data IO = TxtOut String IO
         | TxtIn (String -> IO₂)
         | Terminate

main :: IO
main = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate

Không tệ lắm!

Vì vậy, những gì có tất cả những điều này để làm với các đơn nguyên?

Trong thực tế, bạn sẽ không muốn sử dụng các hàm tạo đơn giản để xác định tất cả các chương trình của mình. Cần phải có một cặp tốt của các nhà xây dựng cơ bản như vậy, nhưng đối với hầu hết các công cụ cấp cao hơn, chúng tôi muốn viết một hàm với một số chữ ký cấp cao đẹp. Hóa ra hầu hết trong số này sẽ trông khá giống nhau: chấp nhận một loại giá trị được đánh máy có ý nghĩa và mang lại kết quả IO.

getTime :: (UTCTime -> IO₂) -> IO
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO

Rõ ràng có một mô hình ở đây, và chúng ta nên viết nó như là

type IO a = (a -> IO₂) -> IO    -- If this reminds you of continuation-passing
                                  -- style, you're right.

getTime :: IO UTCTime
randomRIO :: Random r => (r,r) -> IO r
findFile :: RegEx -> IO (Maybe FilePath)

Bây giờ nó đã bắt đầu trông quen thuộc, nhưng chúng ta vẫn chỉ xử lý các chức năng đơn giản được ngụy trang dưới lớp vỏ bọc và điều đó rất rủi ro: mỗi hành động giá trị hành vi của Cam có trách nhiệm thực sự truyền lại hành động kết quả của bất kỳ chức năng có chứa nào (khác luồng điều khiển của toàn bộ chương trình dễ dàng bị phá vỡ bởi một hành động xấu ở giữa). Chúng ta nên làm cho yêu cầu đó rõ ràng hơn. Chà, hóa ra đó là những luật đơn nguyên , mặc dù tôi không chắc chúng ta có thể thực sự xây dựng chúng mà không cần các toán tử liên kết / tham gia tiêu chuẩn.

Ở bất cứ giá nào, giờ đây chúng tôi đã đạt được một công thức IO có thể hiện đơn nguyên phù hợp:

data IO a = TxtOut String (IO a)
           | TxtIn (String -> IO a)
           | TerminateWith a

txtOut :: String -> IO ()
txtOut s = TxtOut s $ TerminateWith ()

txtIn :: IO String
txtIn = TxtIn $ TerminateWith

instance Functor IO where
  fmap f (TerminateWith a) = TerminateWith $ f a
  fmap f (TxtIn g) = TxtIn $ fmap f . g
  fmap f (TxtOut s c) = TxtOut s $ fmap f c

instance Applicative IO where
  pure = TerminateWith
  (<*>) = ap

instance Monad IO where
  TerminateWith x >>= f = f x
  TxtOut s c >>= f = TxtOut s $ c >>= f
  TxtIn g >>= f = TxtIn $ (>>=f) . g

Rõ ràng đây không phải là một triển khai IO hiệu quả, nhưng về nguyên tắc nó có thể sử dụng được.


@jdlugosz : IO3 a ≡ Cont IO2 a. Nhưng tôi muốn nói rằng nhận xét đó nhiều hơn như một cái gật đầu với những người đã biết đơn vị tiếp tục, vì nó không thực sự có tiếng là thân thiện với người mới bắt đầu.
rẽ trái

4

Monads chỉ là một khung thuận tiện để giải quyết một lớp các vấn đề định kỳ. Đầu tiên, các đơn vị phải là functor (nghĩa là phải hỗ trợ ánh xạ mà không cần nhìn vào các phần tử (hoặc kiểu của chúng)), chúng cũng phải mang lại thao tác ràng buộc (hoặc chuỗi) và cách tạo giá trị đơn trị từ loại phần tử ( return). Cuối cùng, bindreturnphải thỏa mãn hai phương trình (danh tính trái và phải), còn được gọi là luật đơn nguyên. (Ngoài ra, người ta có thể định nghĩa các đơn nguyên có flattening operationthay vì ràng buộc.)

Các danh sách đơn nguyên thường được sử dụng để đối phó với những người không định mệnh. Hoạt động liên kết chọn một thành phần của danh sách (theo trực giác tất cả chúng trong các thế giới song song ), cho phép lập trình viên thực hiện một số tính toán với chúng, sau đó kết hợp các kết quả trong tất cả các thế giới vào danh sách đơn (bằng cách ghép, hoặc làm phẳng, một danh sách lồng nhau ). Đây là cách người ta sẽ định nghĩa một hàm hoán vị trong khung đơn âm của Haskell:

perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

Dưới đây là một ví dụ repl phiên:

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

Cần lưu ý rằng danh sách đơn nguyên không có cách nào là tính toán phụ. Một cấu trúc toán học là một đơn nguyên (nghĩa là tuân thủ các giao diện và luật đã đề cập ở trên) không bao hàm các tác dụng phụ, mặc dù các hiện tượng tác dụng phụ thường rất phù hợp với khung đơn nguyên.


3

Monads phục vụ cơ bản để kết hợp các chức năng với nhau trong một chuỗi. Giai đoạn = Stage.

Bây giờ cách họ sáng tác khác nhau giữa các đơn nguyên hiện có, do đó dẫn đến các hành vi khác nhau (ví dụ: để mô phỏng trạng thái có thể thay đổi trong trạng thái đơn nguyên).

Sự nhầm lẫn về các đơn nguyên là rất chung chung, tức là một cơ chế để soạn thảo các hàm, chúng có thể được sử dụng cho nhiều thứ, do đó mọi người tin rằng các đơn vị là về trạng thái, về IO, v.v., khi chúng chỉ nói về "các chức năng sáng tác ".

Bây giờ, một điều thú vị về các đơn nguyên, đó là kết quả của bố cục luôn thuộc loại "M a", nghĩa là, một giá trị bên trong một phong bì được gắn thẻ "M". Tính năng này thực sự rất hay khi thực hiện, ví dụ, một sự tách biệt rõ ràng giữa thuần túy với mã không tinh khiết: khai báo tất cả các hành động không tinh khiết là các hàm của loại "IO a" và không cung cấp chức năng nào, khi xác định đơn vị IO, để loại bỏ " một "giá trị từ bên trong" IO a ". Kết quả là không có chức năng nào có thể thuần túy và đồng thời lấy ra một giá trị từ "IO a", bởi vì không có cách nào để lấy giá trị đó trong khi vẫn giữ nguyên (hàm phải nằm trong đơn vị "IO" để sử dụng giá trị đó). (LƯU Ý: tốt, không có gì là hoàn hảo, do đó, "bó IO" có thể bị phá vỡ bằng cách sử dụng "unsafePerformIO: IO a -> a"


2

Bạn cần các đơn nguyên nếu bạn có một hàm tạo kiểu và các hàm trả về các giá trị của họ kiểu đó . Cuối cùng, bạn muốn kết hợp các loại chức năng này với nhau . Đây là ba yếu tố chính để trả lời tại sao .

Hãy để tôi giải thích. Bạn có Int, Stringvà các Realchức năng của loại Int -> String, String -> Realv.v. Bạn có thể kết hợp các chức năng này một cách dễ dàng, kết thúc bằng Int -> Real. Cuộc sống là tốt.

Sau đó, một ngày nào đó, bạn cần phải tạo ra một mới gia đình các loại . Có thể là do bạn cần xem xét khả năng trả về không có giá trị ( Maybe), trả về lỗi ( Either), nhiều kết quả ( List), v.v.

Lưu ý rằng đó Maybelà một constructor loại. Nó có một loại, thích Intvà trả về một loại mới Maybe Int. Điều đầu tiên cần nhớ, không có loại xây dựng, không có đơn nguyên.

Tất nhiên, bạn muốn sử dụng hàm tạo kiểu trong mã của mình và sớm kết thúc với các hàm như Int -> Maybe StringString -> Maybe Float. Bây giờ, bạn không thể dễ dàng kết hợp các chức năng của mình. Cuộc sống không còn tốt nữa.

Và đây là khi các đơn vị đến giải cứu. Chúng cho phép bạn kết hợp loại chức năng đó một lần nữa. Bạn chỉ cần thay đổi thành phần . cho > == .


2
Điều này không có gì để làm với các gia đình loại. Bạn thực sự đang nói về cái gì?
dfeuer
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.