Tại sao các tác dụng phụ được mô hình hóa thành các đơn nguyên trong Haskell?


172

Bất cứ ai cũng có thể đưa ra một số gợi ý về lý do tại sao các tính toán không tinh khiết trong Haskell được mô hình hóa thành các đơn nguyên?

Ý tôi là monad chỉ là một giao diện với 4 thao tác, vậy lý do để mô hình hóa các hiệu ứng phụ trong đó là gì?


15
Monads chỉ xác định hai hoạt động.
Dario

3
Nhưng những gì về trở lại và thất bại? (bên cạnh (>>) và (>> =))
bodacydo

55
Hai hoạt động là return(>>=). x >> ylà giống như x >>= \\_ -> y(tức là nó bỏ qua kết quả của đối số đầu tiên). Chúng ta không nói về fail.
porges

2
@Porges Tại sao không nói về thất bại? Nó hơi hữu ích trong nghĩa là Có thể, Trình phân tích cú pháp, v.v.
thay thế

16
@monadic: failđang ở trong Monadlớp vì một tai nạn lịch sử; nó thực sự thuộc về MonadPlus. Hãy lưu ý rằng định nghĩa mặc định của nó là không an toàn.
JB.

Câu trả lời:


292

Giả sử một chức năng có tác dụng phụ. Nếu chúng ta lấy tất cả các hiệu ứng mà nó tạo ra làm tham số đầu vào và đầu ra, thì hàm đó hoàn toàn là thế giới bên ngoài.

Vì vậy, đối với một chức năng không tinh khiết

f' :: Int -> Int

chúng tôi thêm RealWorld vào xem xét

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

sau đó flà tinh khiết trở lại. Chúng tôi xác định loại dữ liệu được tham số hóa type IO a = RealWorld -> (a, RealWorld), vì vậy chúng tôi không cần nhập RealWorld nhiều lần và chỉ có thể viết

f :: Int -> IO Int

Đối với lập trình viên, việc xử lý trực tiếp một RealWorld là quá nguy hiểm, đặc biệt, nếu một lập trình viên nắm trong tay giá trị của loại RealWorld, họ có thể cố gắng sao chép nó, điều này về cơ bản là không thể. (Ví dụ, nghĩ về việc cố gắng sao chép toàn bộ hệ thống tệp. Bạn sẽ đặt nó ở đâu?) Do đó, định nghĩa về IO của chúng tôi cũng gói gọn các trạng thái của toàn thế giới.

Thành phần của các chức năng "không tinh khiết"

Các hàm không tinh khiết này là vô dụng nếu chúng ta không thể xâu chuỗi chúng lại với nhau. Xem xét

getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)

Chúng tôi muốn

  • lấy tên tệp từ bàn điều khiển
  • đọc tập tin đó và
  • in nội dung của tập tin đó lên bàn điều khiển.

Làm thế nào chúng ta sẽ làm điều đó nếu chúng ta có thể truy cập vào các quốc gia trong thế giới thực?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

Chúng tôi thấy một mô hình ở đây. Các chức năng được gọi như thế này:

...
(<result-of-f>, worldY) = f               worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

Vì vậy, chúng ta có thể định nghĩa một toán tử ~~~để ràng buộc chúng:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b,   RealWorld))
      ->                    (b -> RealWorld -> (c, RealWorld))
      ->      (RealWorld                    -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
                   in g resF worldY

sau đó chúng ta có thể viết

printFile = getLine ~~~ getContents ~~~ putStrLn

mà không chạm vào thế giới thực.

"Impurization"

Bây giờ giả sử chúng ta muốn làm cho chữ hoa nội dung tập tin là tốt. Uppercasing là một chức năng thuần túy

upperCase :: String -> String

Nhưng để làm cho nó vào thế giới thực, nó phải trả lại một IO String. Thật dễ dàng để nâng một chức năng như vậy:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

Điều này có thể được khái quát:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

vì vậy impureUpperCase = impurify . upperCase, và chúng ta có thể viết

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(Lưu ý: Thông thường chúng tôi viết getLine ~~~ getContents ~~~ (putStrLn . upperCase) )

Chúng tôi đã làm việc với các đơn vị cùng

Bây giờ hãy xem những gì chúng tôi đã làm:

  1. Chúng tôi đã định nghĩa một toán tử (~~~) :: IO b -> (b -> IO c) -> IO ckết hợp hai hàm không tinh khiết với nhau
  2. Chúng tôi đã định nghĩa một hàm impurify :: a -> IO achuyển đổi một giá trị thuần túy thành không tinh khiết.

Bây giờ chúng tôi thực hiện nhận dạng (>>=) = (~~~)return = impurify, xem? Chúng tôi đã có một đơn nguyên.


Ghi chú kỹ thuật

Để đảm bảo nó thực sự là một đơn nguyên, vẫn còn một số tiên đề cần được kiểm tra:

  1. return a >>= f = f a

     impurify a                =  (\world -> (a, world))
    (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                  in f resF worldY
                               =  let (resF, worldY) =            (a, worldX)       
                                  in f resF worldY
                               =  f a worldX
  2. f >>= return = f

    (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                in impurify resF worldY
                             =  let (resF, worldY) = f worldX      
                                in (resF, worldY)
                             =  f worldX
  3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    Còn lại như tập thể dục.


5
+1 nhưng tôi muốn lưu ý rằng điều này đặc biệt bao gồm trường hợp IO. blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html khá giống nhau, nhưng khái quát RealWorldthành ... tốt, bạn sẽ thấy.
ephemient

4
Lưu ý rằng giải thích này thực sự không thể áp dụng cho Haskell IO, bởi vì phần sau hỗ trợ tương tác, đồng thời và không thuyết phục. Xem câu trả lời của tôi cho câu hỏi này để biết thêm một số gợi ý.
Conal

2
@Conal GHC thực sự thực hiện IOtheo cách này, nhưng RealWorldthực tế không đại diện cho thế giới thực, nó chỉ là một mã thông báo để giữ cho các hoạt động theo thứ tự ("ma thuật" là RealWorldloại duy nhất duy nhất của GHC Haskell)
Danh sách Jeremy

2
@JeremyList Theo tôi hiểu, GHC thực hiện IOthông qua sự kết hợp giữa biểu diễn này và ma thuật trình biên dịch không chuẩn (gợi nhớ đến virus biên dịch C nổi tiếng của Ken Thompson ). Đối với các loại khác, sự thật nằm trong mã nguồn cùng với ngữ nghĩa Haskell thông thường.
Conal

1
@Clonal Nhận xét của tôi là do tôi đã đọc các phần có liên quan của mã nguồn GHC.
Danh sách Jeremy

43

Bất cứ ai cũng có thể đưa ra một số gợi ý về lý do tại sao các tính toán không rõ ràng trong Haskell được mô hình hóa thành các đơn nguyên?

Câu hỏi này chứa một sự hiểu lầm phổ biến. Tạp chất và Monad là những quan niệm độc lập. Tạp chất không được mô hình hóa bởi Monad. Thay vào đó, có một vài loại dữ liệu, chẳng hạn như IO, đại diện cho tính toán bắt buộc. Và đối với một số loại, một phần nhỏ giao diện của chúng tương ứng với mẫu giao diện có tên là "Monad". Hơn nữa, không có lời giải thích thuần túy / chức năng / biểu thị nào được biết đến IO(và không có khả năng là một, xem xét mục đích "thùng tội lỗi"IO ), mặc dù có câu chuyện thường được nói về World -> (a, World)ý nghĩa của nó IO a. Câu chuyện đó không thể mô tả một cách trung thực IO, bởi vìIOhỗ trợ đồng thời và không thuyết phục. Câu chuyện thậm chí không hoạt động khi cho các tính toán xác định cho phép tương tác giữa tính toán với thế giới.

Để giải thích thêm, xem câu trả lời này .

Chỉnh sửa : Khi đọc lại câu hỏi, tôi không nghĩ câu trả lời của mình khá đúng hướng. Các mô hình tính toán bắt buộc thường biến thành các đơn nguyên, giống như câu hỏi đã nói. Người hỏi có thể không thực sự cho rằng sự đơn điệu trong bất kỳ cách nào cho phép mô hình hóa tính toán bắt buộc.


1
@KennyTM: Nhưng RealWorld, như các tài liệu nói, "sâu sắc kỳ diệu". Đó là một mã thông báo đại diện cho những gì hệ thống thời gian chạy đang làm, nó thực sự không có ý nghĩa gì về thế giới thực. Bạn thậm chí không thể tạo ra một cái mới để tạo ra một "chủ đề" mà không cần thực hiện thêm thủ thuật nào; Cách tiếp cận ngây thơ sẽ chỉ tạo ra một hành động, chặn duy nhất với nhiều sự mơ hồ về thời điểm nó sẽ chạy.
CA McCann

4
Ngoài ra, tôi sẽ tranh luận rằng các đơn nguyên về bản chất là bắt buộc. Nếu functor đại diện cho một số cấu trúc với các giá trị được nhúng trong nó, thì một thể hiện đơn nguyên có nghĩa là bạn có thể xây dựng và làm phẳng các lớp mới dựa trên các giá trị đó. Vì vậy, bất kể ý nghĩa nào bạn gán cho một lớp duy nhất của functor, một đơn vị có nghĩa là bạn có thể tạo một số lượng các lớp không giới hạn với một khái niệm nghiêm ngặt về quan hệ nhân quả đi từ lớp này sang lớp kế tiếp. Các trường hợp cụ thể có thể không có cấu trúc bắt buộc thực chất, nhưng Monadnói chung thực sự có.
CA McCann

3
" MonadNói chung" tôi có nghĩa là đại khái forall m. Monad m => ..., nghĩa là, làm việc trên một trường hợp tùy ý. Những điều bạn có thể làm với một đơn nguyên tùy ý gần như chính xác những điều bạn có thể làm với IO: nhận các nguyên hàm mờ (như các đối số chức năng, hoặc từ các thư viện, tương ứng), xây dựng các returngiá trị không thay đổi hoặc sử dụng một giá trị theo cách không thể đảo ngược bằng cách sử dụng (>>=). Bản chất của lập trình trong một đơn nguyên tùy ý là tạo ra một danh sách các hành động không thể hủy bỏ: "làm X, sau đó làm Y, sau đó ...". Âm thanh khá cấp bách đối với tôi!
CA McCann

2
Không, bạn vẫn còn thiếu quan điểm của tôi ở đây. Tất nhiên bạn sẽ không sử dụng tư duy đó cho bất kỳ loại cụ thể nào, bởi vì chúng có cấu trúc rõ ràng, có ý nghĩa. Khi tôi nói "đơn nguyên tùy ý", ý tôi là "bạn không được chọn cái nào"; viễn cảnh ở đây là từ bên trong bộ định lượng, do đó, suy nghĩ về sự mtồn tại có thể hữu ích hơn. Hơn nữa, "giải thích" của tôi là một sự sửa đổi lại của pháp luật; danh sách các câu lệnh "do X" chính xác là đơn vị tự do trên cấu trúc không xác định được tạo thông qua (>>=); và các luật đơn nguyên chỉ là các luật đơn trị về thành phần endofunctor.
CA McCann

3
Nói tóm lại, giới hạn dưới lớn nhất về những gì tất cả các đơn vị cùng mô tả là một cuộc diễu hành mù quáng, vô nghĩa vào tương lai. IOlà một trường hợp bệnh lý chính xác bởi vì nó cung cấp hầu như không có gì nhiều hơn mức tối thiểu này. Trong các trường hợp cụ thể, các loại có thể tiết lộ nhiều cấu trúc hơn và do đó có ý nghĩa thực tế; nhưng mặt khác, các tính chất cần thiết của một đơn nguyên - dựa trên luật pháp - là phản đối với việc biểu thị rõ ràng như IOhiện tại. Không xuất khẩu các nhà xây dựng, liệt kê triệt để các hành động nguyên thủy, hoặc một cái gì đó tương tự, tình hình là vô vọng.
CA McCann

13

Theo tôi hiểu, một người được gọi là Eugenio Moggi trước tiên nhận thấy rằng một cấu trúc toán học tối nghĩa trước đây được gọi là "đơn nguyên" có thể được sử dụng để mô hình hóa các hiệu ứng phụ trong ngôn ngữ máy tính, và do đó chỉ định ngữ nghĩa của chúng bằng phép tính Lambda. Khi Haskell đang được phát triển, có nhiều cách khác nhau trong đó các tính toán không tinh khiết được mô hình hóa (xem bài báo "áo sơ mi tóc" của Simon Peyton Jones để biết thêm chi tiết), nhưng khi Phil Wadler giới thiệu các đơn vị thì rõ ràng đây là Câu trả lời. Và phần còn lại là lịch sử.


3
Không hẳn. Người ta đã biết rằng một đơn nguyên có thể mô hình hóa việc giải thích trong một thời gian rất dài (ít nhất là từ "Topoi: Phân tích logic phân loại). Mặt khác, không thể diễn tả rõ ràng các loại cho các đơn vị cho đến khi chức năng được gõ mạnh ngôn ngữ đến xung quanh, và sau đó Moggi đưa hai và hai lại với nhau.
Tên của

1
Có lẽ các đơn vị có thể dễ hiểu hơn nếu chúng được xác định theo nghĩa bao bọc bản đồ và mở ra với sự trở lại là một từ đồng nghĩa với bọc.
aoeu256

9

Bất cứ ai cũng có thể đưa ra một số gợi ý về lý do tại sao các tính toán không rõ ràng trong Haskell được mô hình hóa thành các đơn nguyên?

Vâng, bởi vì Haskell là tinh khiết . Bạn cần một khái niệm toán học để phân biệt giữa các tính toán không rõ ràngcác tính toán thuần túycấp độ loại và để mô hình hóa các luồng chương trình tương ứng.

Điều này có nghĩa là bạn sẽ phải kết thúc với một số loại IO amô hình tính toán không chắc chắn. Sau đó, bạn cần biết các cách kết hợp các tính toán này áp dụng theo trình tự ( >>=) và nâng một giá trị (return ) là những cách cơ bản và rõ ràng nhất.

Với hai điều này, bạn đã xác định được một đơn nguyên (thậm chí không nghĩ về nó);)

Ngoài ra, các đơn nguyên cung cấp sự trừu tượng rất chung chung và mạnh mẽ , vì vậy nhiều loại luồng điều khiển có thể được khái quát hóa một cách thuận tiện trong các chức năng đơn nguyên như sequence,liftM hoặc cú pháp đặc biệt, làm cho sự không rõ ràng không phải là trường hợp đặc biệt.

Xem các đơn nguyên trong lập trình chức nănggõ duy nhất (cách thay thế duy nhất tôi biết) để biết thêm thông tin.


6

Như bạn nói, Monadlà một cấu trúc rất đơn giản. Một nửa câu trả lời là: Monadlà cấu trúc đơn giản nhất mà chúng ta có thể cung cấp cho các chức năng tác dụng phụ và có thể sử dụng chúng. Với Monadchúng ta có thể làm hai việc: chúng ta có thể coi giá trị thuần là giá trị tác dụng phụ ( return) và chúng ta có thể áp dụng hàm tác dụng phụ cho giá trị tác dụng phụ để có giá trị tác dụng phụ mới ( >>=). Mất khả năng làm một trong những điều này sẽ làm tê liệt, vì vậy loại tác dụng phụ của chúng ta cần phải "ít nhất" Monad, và hóa raMonad là đủ để thực hiện mọi thứ chúng ta cần cho đến nay.

Nửa còn lại là: cấu trúc chi tiết nhất chúng ta có thể cung cấp cho "tác dụng phụ có thể xảy ra" là gì? Chúng tôi chắc chắn có thể nghĩ về không gian của tất cả các tác dụng phụ có thể có dưới dạng một tập hợp (hoạt động duy nhất yêu cầu là thành viên). Chúng ta có thể kết hợp hai tác dụng phụ bằng cách thực hiện lần lượt từng tác dụng phụ và điều này sẽ tạo ra hiệu ứng phụ khác (hoặc có thể giống nhau - nếu lần đầu tiên là "tắt máy tính" và lần thứ hai là "ghi tệp", thì kết quả là của việc soạn thảo chúng chỉ là "máy tính tắt").

Ok, vậy chúng ta có thể nói gì về hoạt động này? Đó là sự kết hợp; nghĩa là, nếu chúng ta kết hợp ba tác dụng phụ, chúng ta sẽ không kết hợp theo thứ tự nào. Nếu chúng ta làm (ghi tệp rồi đọc ổ cắm) rồi tắt máy tính, thì cũng giống như thực hiện ghi tệp sau đó (đọc ổ cắm rồi tắt máy máy vi tính). Nhưng nó không giao hoán: ("ghi tệp" rồi "xóa tệp") là một tác dụng phụ khác với ("xóa tệp" rồi "ghi tệp"). Và chúng tôi có một danh tính: hiệu ứng phụ đặc biệt "không có tác dụng phụ" hoạt động ("không có tác dụng phụ" thì "xóa tệp" là tác dụng phụ giống như "xóa tệp") Tại thời điểm này, bất kỳ nhà toán học nào cũng nghĩ "Nhóm!" Nhưng các nhóm có nghịch đảo và không có cách nào để đảo ngược tác dụng phụ nói chung; "xóa tài liệu" là không thể đảo ngược. Vì vậy, cấu trúc chúng ta còn lại là của một monoid, có nghĩa là các chức năng phụ của chúng ta phải là các đơn nguyên.

Có một cấu trúc phức tạp hơn? Chắc chắn rồi! Chúng tôi có thể phân chia các tác dụng phụ có thể thành các hiệu ứng dựa trên hệ thống tập tin, hiệu ứng dựa trên mạng và hơn thế nữa, và chúng tôi có thể đưa ra các quy tắc bố cục phức tạp hơn để bảo tồn các chi tiết này. Nhưng một lần nữa, nó lại xuất hiện: Monadrất đơn giản và đủ mạnh để thể hiện hầu hết các tính chất mà chúng ta quan tâm. (Đặc biệt, tính kết hợp và các tiên đề khác cho phép chúng tôi kiểm tra ứng dụng của chúng tôi thành từng phần nhỏ, với sự tự tin rằng các tác dụng phụ của ứng dụng kết hợp sẽ giống như sự kết hợp các tác dụng phụ của các mảnh).


4

Đó thực sự là một cách khá rõ ràng để nghĩ về I / O một cách có chức năng.

Trong hầu hết các ngôn ngữ lập trình, bạn thực hiện các thao tác nhập / xuất. Trong Haskell, hãy tưởng tượng viết mã không phải để thực hiện các thao tác, mà là để tạo một danh sách các hoạt động mà bạn muốn làm.

Monads chỉ là cú pháp đẹp cho chính xác điều đó.

Nếu bạn muốn biết lý do tại sao các đơn vị trái ngược với điều gì khác, tôi đoán câu trả lời là chúng là cách tốt nhất để thể hiện I / O mà mọi người có thể nghĩ đến khi họ tạo ra Haskell.


3

AFAIK, lý do là để có thể bao gồm kiểm tra tác dụng phụ trong hệ thống loại. Nếu bạn muốn biết thêm, hãy nghe các tập SE-Radio đó : Tập 108: Simon Peyton Jones về Lập trình chức năng và Haskell Tập 72: Erik Meijer trên LINQ


2

Trên đây là những câu trả lời chi tiết rất tốt với nền tảng lý thuyết. Nhưng tôi muốn đưa ra quan điểm của tôi về IO monad. Tôi không có kinh nghiệm lập trình haskell, vì vậy có thể nó khá ngây thơ hoặc thậm chí sai. Nhưng tôi đã giúp tôi đối phó với IO monad ở một mức độ nào đó (lưu ý rằng nó không liên quan đến các đơn nguyên khác).

Đầu tiên tôi muốn nói, ví dụ đó với "thế giới thực" không quá rõ ràng đối với tôi vì chúng ta không thể truy cập vào các trạng thái trước đó (thế giới thực). Có thể nó không liên quan đến các tính toán đơn nguyên nhưng nó được mong muốn theo nghĩa minh bạch tham chiếu, thường được trình bày trong mã haskell.

Vì vậy, chúng tôi muốn ngôn ngữ của chúng tôi (haskell) là thuần khiết. Nhưng chúng tôi cần các hoạt động đầu vào / đầu ra vì không có chúng, chương trình của chúng tôi không thể hữu ích. Và những hoạt động đó không thể thuần túy bởi bản chất của chúng. Vì vậy, cách duy nhất để giải quyết vấn đề này là chúng ta phải tách các hoạt động không tinh khiết khỏi phần còn lại của mã.

Ở đây đơn nguyên đến. Trên thực tế, tôi không chắc chắn, rằng không thể tồn tại cấu trúc khác với các thuộc tính cần thiết tương tự, nhưng điểm quan trọng là đơn nguyên có các thuộc tính này, vì vậy nó có thể được sử dụng (và nó được sử dụng thành công). Tài sản chính là chúng ta không thể thoát khỏi nó. Giao diện đơn nguyên không có hoạt động để loại bỏ các đơn vị xung quanh giá trị của chúng tôi. Các đơn nguyên khác (không phải IO) cung cấp các hoạt động như vậy và cho phép khớp mẫu (ví dụ Có thể), nhưng các hoạt động đó không có trong giao diện đơn nguyên. Một tài sản cần thiết khác là khả năng hoạt động chuỗi.

Nếu chúng ta nghĩ về những gì chúng ta cần về hệ thống loại, chúng ta sẽ đi đến thực tế là chúng ta cần loại với hàm tạo, có thể được bao quanh bất kỳ vale nào. Trình xây dựng phải ở chế độ riêng tư, vì chúng tôi cấm thoát khỏi nó (tức là khớp mẫu). Nhưng chúng ta cần chức năng để đưa giá trị vào hàm tạo này (ở đây trở lại trong tâm trí). Và chúng ta cần cách để hoạt động chuỗi. Nếu chúng ta nghĩ về nó một thời gian, chúng ta sẽ đi đến thực tế, rằng hoạt động chuỗi phải có kiểu như >> = has. Vì vậy, chúng tôi đến một cái gì đó rất giống với đơn nguyên. Tôi nghĩ, nếu bây giờ chúng ta phân tích các tình huống mâu thuẫn có thể xảy ra với cấu trúc này, chúng ta sẽ đến với các tiên đề đơn nguyên.

Lưu ý, cấu trúc được phát triển đó không có gì chung với tạp chất. Nó chỉ có các thuộc tính, mà chúng tôi mong muốn có thể xử lý các hoạt động không tinh khiết, cụ thể là, không thoát, xích và một cách để vào trong.

Bây giờ một số tập hợp các hoạt động không tinh khiết được xác định trước bởi ngôn ngữ trong IO đơn nguyên được chọn này. Chúng ta có thể kết hợp các hoạt động đó để tạo ra các hoạt động không rõ ràng mới. Và tất cả các hoạt động đó sẽ phải có IO trong loại của họ. Tuy nhiên, lưu ý rằng sự hiện diện của IO trong một số chức năng không làm cho chức năng này không trong sạch. Nhưng theo tôi hiểu, việc viết các hàm thuần túy với IO theo kiểu của họ là ý tưởng tồi, vì ban đầu ý tưởng của chúng tôi là tách các hàm thuần túy và không tinh khiết.

Cuối cùng, tôi muốn nói rằng, đơn nguyên đó không biến các hoạt động không tinh khiết thành các hoạt động thuần túy. Nó chỉ cho phép tách chúng một cách hiệu quả. (Tôi nhắc lại, đó chỉ là sự hiểu biết của tôi)


1
Chúng giúp bạn gõ kiểm tra chương trình của bạn bằng cách cho phép bạn nhập hiệu ứng kiểm tra và bạn có thể xác định DSL của riêng mình bằng cách tạo các đơn vị để hạn chế các hiệu ứng mà chức năng của bạn có thể thực hiện để trình biên dịch có thể kiểm tra lỗi trình tự của bạn.
aoeu256

Nhận xét này từ aoeu256 là "tại sao" thiếu trong tất cả các giải thích được đưa ra cho đến nay. (ví dụ: các đơn nguyên không dành cho con người, nhưng dành cho trình biên dịch)
João Otero
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.