Skip to main content

Bài 4: Monads

Ghi chú
  • Đây là phiên bản viết của Bài giảng số 4 .
  • Trong bài giảng này, chúng ta tìm hiểu về Monad. Đặc biệt là các monads EmulatorTrace và Contract.

4.1. Tổng quan

Chúng tôi đã dành hai bài giảng cuối cùng để nói về phần on-chain của Plutus - logic xác thực được biên dịch thành tập lệnh Plutus và thực sự sống trên blockchain và được thực hiện bởi các nút xác thực giao dịch.

Còn rất nhiều điều để nói về phần on-chain đó.

Chúng tôi chưa xem xét các ví dụ phức tạp hơn về xác thực sử dụng ngữ cảnh phức tạp hơn và chúng tôi chưa thấy cách mã thông báo gốc hoạt động như thế nào (tập lệnh Plutus cũng được sử dụng để xác thực việc đúc và đốt mã thông báo gốc).

Chúng ta chắc chắn sẽ phải nói về những chủ đề đó, và quay lại vấn đề đó.

Tuy nhiên, trước khi đi vào quá nhiều chủ đề phức tạp về xác thực trên chuỗi, chúng ta không được bỏ qua phần ngoài chuỗi, vì nó cũng quan trọng không kém.

Phần on-chain sẽ đảm nhận việc xác thực nhưng để có thứ gì đó được xác thực, chúng ta phải xây dựng một giao dịch và gửi nó lên blockchain. Và, đó là những gì phần off-chain thực hiện.

Vì vậy, chúng ta sẽ bắt đầu nói về cách viết mã Plutus ngoài chuỗi.

Thật không may, có một vấn đề nhỏ liên quan đến các tính năng Haskell cần thiết.

Phần on-chain mà chúng ta đã thấy cho đến nay hơi xa lạ và phải làm quen một chút, do thực tế là chúng ta có thêm sự phức tạp của quá trình biên dịch sang tập lệnh Plutus. Nhưng, chúng ta không thực sự phải lo lắng về điều đó nếu chúng ta sử dụng phép thuật Template Haskell. Trong trường hợp đó, hàm xác nhận chỉ là một hàm đơn giản.

Và nó thực sự là một chức năng Haskell rất đơn giản theo quan điểm kỹ thuật. Chúng tôi không sử dụng bất kỳ tính năng Haskell ưa thích nào để viết hàm này.

Một trong những lý do cho điều đó là cách thức hoạt động của Plutus. Chúng tôi đã thấy làm thế nào, để quá trình biên dịch sang Plutus thành công, tất cả mã được sử dụng bởi hàm xác nhận phải có sẵn trong Oxford Brackets. Điều này có nghĩa là tất cả các chức năng được sử dụng bởi hàm mkValidator phải sử dụng pragma INLINABLE.

*{-# INLINABLE mkValidator #-}*
mkValidator **::** Data **\->** Data **\->** Data **\->** ()
mkValidator **\_** **\_** **\_** **\=** ()

$$(PlutusTx.compile \[|| mkValidator ||\])

Và hãy nhớ lại, vì các hàm Haskell tiêu chuẩn không có pragma CÓ THỂ LỆNH này, nên có một mô-đun Plutus Prelude mới tương tự như Haskell Prelude tiêu chuẩn, nhưng với các chức năng được xác định bằng pragma INLINABLE.

Nhưng, tất nhiên, có hàng trăm thư viện Haskell ngoài kia và hầu hết chúng không được viết với Plutus, vì vậy chúng tôi không thể sử dụng chúng trong quá trình xác thực. Và, điều đó có tác dụng là xác thực bên trong Haskell sẽ tương đối đơn giản và sẽ không có nhiều phụ thuộc.

Trong phần off-chain của Plutus, tình hình đã đảo ngược. Chúng ta không phải lo lắng về việc biên dịch sang tập lệnh Plutus - nó chỉ đơn giản là Haskell. Nhưng, mặt trái của nó là, cách thực hiện điều này, nó sử dụng các tính năng Haskell phức tạp hơn nhiều - ví dụ như cái gọi là hệ thống hiệu ứng, phát trực tuyến và đặc biệt là các monads.

Tất cả mã off-chain (mã ví), được viết bằng một Monad đặc biệt - Monad hợp đồng.

Các tu viện nổi tiếng trong thế giới Haskell. Đây thường là trở ngại đầu tiên khi bắt đầu lập trình viên Haskell.

Có rất nhiều hướng dẫn cố gắng giải thích các Monads. Monads được so sánh với burritos, và tất cả các loại ẩn dụ được sử dụng để cố gắng giải thích khái niệm. Nhưng ở đây, ít nhất chúng ta hãy cố gắng cung cấp một khóa học cơ bản về monads cho những người mới sử dụng Haskell.

Trước khi đi đến các monads chung, chúng ta sẽ bắt đầu với IO , đó là cách xử lý các tác dụng phụ của IO trong Haskell. Tuy nhiên, trước khi đến với Haskell, chúng ta hãy xem xét một ngôn ngữ chính như Java.

Hãy xem xét phương pháp Java sau đây.

**public** **static** int foo() {
...
}

Hàm này không có đối số và nó trả về một int. Hãy tưởng tượng nó được gọi hai lần trong mã.

...
**final** int a \= foo();
...
**final** int b \= foo();

Bây giờ, chúng ta lưu ý rằng, miễn là chúng ta không biết điều gì đang xảy ra bên trong hàm foo (), giá trị trả về của biểu thức sau là không xác định.

a \== b; *// true or false? at compile time, we don't know*

Chúng tôi không biết có agiống như bvậy không vì trong Java, hoàn toàn có thể xảy ra một số IO bên trong foo. Ví dụ: có mã là mã yêu cầu người dùng nhập đầu vào trên bảng điều khiển và sử dụng mã này để tính toán giá trị trả về.

Điều này có nghĩa là, để lập luận về mã, chúng ta cần phải xem xét bên trong foo, điều này làm cho việc thử nghiệm trở nên khó khăn hơn. Và nó có nghĩa là, fooví dụ, đó là lệnh gọi trả về đầu tiên 13- chúng ta không thể thay thế tất cả các lệnh gọi khác đến foobằng giá trị trả về đã biết của 13.

Ở Haskell, tình hình rất khác vì Haskell là một ngôn ngữ chức năng thuần túy. Chữ ký tương đương trong Haskell sẽ giống như sau:

foo **::** Int
foo **\=** ...

Bây giờ, nếu chúng ta gặp trường hợp chúng ta gọi foohai lần, mặc dù chúng ta không biết giá trị của foolà gì, chúng ta biết chắc chắn rằng hai giá trị trả về sẽ giống nhau.

Đây là một tính năng rất quan trọng được gọi là tính minh bạch tham chiếu . Trên thực tế, có một số cửa thoát hiểm để giải quyết vấn đề này, nhưng chúng ta có thể bỏ qua điều này.

Điều này làm cho các tác vụ như tái cấu trúc và thử nghiệm dễ dàng hơn nhiều.

Điều này là rất tốt, nhưng bạn cần phải có tác dụng phụ để có ảnh hưởng đến thế giới. Nếu không, tất cả những gì chương trình của bạn làm là làm nóng bộ xử lý.

Bạn cần đầu vào và đầu ra. Bạn phải có khả năng ghi đầu ra ra màn hình, hoặc đọc đầu vào từ bàn phím, kết nối mạng hoặc tệp, chẳng hạn.

Có một video nổi tiếng của Simon Peyton-Jones tên là Haskell Is Useless , giải thích rằng ngôn ngữ thuần túy, không có tác dụng phụ thì rất đẹp về mặt toán học, nhưng cuối cùng thì bạn cũng cần có tác dụng phụ để biến mọi thứ thành hiện thực.

Và Haskell có một cách để xử lý các tác dụng phụ và đó là Monad IO. Tuy nhiên, đừng lo lắng về phần Monad.

Đây là cách chúng tôi làm điều đó trong Haskell.

foo **::** IO Int
foo **\=** ...

IO là một phương thức khởi tạo kiểu nhận một đối số, giống như một số ví dụ khác về các hàm tạo kiểu như Có thể và Danh sách . Tuy nhiên, không giống như những ví dụ đó, IO đặc biệt, theo nghĩa là bạn không thể triển khai nó bằng chính ngôn ngữ. Nó là một nguyên thủy được tích hợp sẵn.

Giá trị trả về IO Int cho chúng ta biết rằng đây là công thức để tính Int và công thức này có thể gây ra tác dụng phụ. Danh sách các hướng dẫn cho máy tính biết phải làm gì để kết thúc với Int .

Điều quan trọng cần lưu ý là tính minh bạch của tham chiếu không bị phá vỡ ở đây. Kết quả đánh giá foo là chính công thức, không phải giá trị Int . Và vì công thức luôn giống nhau, nên tính minh bạch của tham chiếu được duy trì.

Cách duy nhất để thực thi một công thức như vậy trong chương trình Haskell là từ điểm nhập chính của chương trình - hàm chính . Bạn cũng có thể thực hiện các hành động IO trong REPL.

Hello World in Haskell trông như thế này:

main **::** IO ()
main **\=** putStrLn "Hello, world!"

Ở đây, main là một công thức thực hiện một số tác dụng phụ và trả về Đơn vị - không có gì đáng quan tâm.

Hãy xem putStrLn trong REPL. Chúng tôi thấy rằng đó là một hành động IO sử dụng một Chuỗi và không trả về kết quả thú vị nào.

Prelude Week04.Contract\> :t putStrLn
putStrLn **::** String **\->** IO ()

Prelude Week04.Contract\> :t putStrLn "Hello, world!"
putStrLn "Hello, world!" **::** IO ()

Chúng tôi cũng có thể chạy điều này. Mở ứng dụng / tệp Main.sh và chỉnh sửa chức năng chính để nó đọc:

main **::** IO ()
main **\=** putStrLn "Hello, world!"

Sau đó chạy

cabal run hello

Chúng ta sẽ xem xét nhanh tập tin cabal ngay bây giờ.

Trong các bài giảng trước, chúng ta chỉ cần phần thư viện trong tệp plutus-pioneer-program-week04.cabal vì chúng ta chỉ xử lý các hàm thư viện. Bây giờ, chúng ta cần thêm một khổ thơ có thể thực thi được .

executable hello
hs-source-dirs: app
main-is: hello.hs
build-depends: base ^>=4.14.1.0
default-language: Haskell2010
ghc-options: -Wall -O2

Điều này chỉ định thư mục nguồn và tệp nào giữ chức năng chính. Thông thường tên tệp phải khớp với tên mô-đun, nhưng chính là một ngoại lệ.

Thay vì chỉ yêu cầu loại putStrLn , chúng ta có thể chạy nó trong REPL. Như đã đề cập, REPL cho phép chúng ta thực hiện các hành động IO.

Prelude Week04.Contract\> putStrLn "Hello, world!"
Hello, world!

Hãy xem getLine

Prelude Week04.Contract\> :t getLine
getLine **::** IO String

Điều này cho thấy rằng đó là một công thức, có thể tạo ra các hiệu ứng phụ, khi được thực thi sẽ tạo ra một Chuỗi . Trong trường hợp getLine , tác dụng phụ được đề cập là nó sẽ đợi người dùng nhập từ bàn phím.

Nếu chúng ta thực thi getLine trong REPL.

Prelude Week04.Contract\> getLine

Nó chờ nhập bàn phím. Sau đó, nếu chúng ta nhập một cái gì đó, nó sẽ trả về kết quả.

Haskell
"Haskell"

Có rất nhiều hành động IO được định nghĩa trong Haskell để thực hiện tất cả các loại như đọc tệp, ghi tệp, đọc từ và ghi vào ổ cắm.

Nhưng cho dù bạn có bao nhiêu hành động được xác định trước, điều đó sẽ không bao giờ là đủ để đạt được điều gì đó phức tạp, vì vậy cần phải có cách để kết hợp các hành động IO nguyên thủy này thành những công thức lớn hơn, phức tạp hơn.

Một điều chúng ta có thể làm là sử dụng thể hiện loại Functor của IO. Hãy xem xét các trường hợp loại của IO trong REPL.

Prelude Week04.Contract\> :i IO
**type** IO **::** \* **\->** \*
**newtype** IO a
**\=** ghc\-prim\-0.6.1:GHC.Types.IO (ghc\-prim\-0.6.1:GHC.Prim.State#
ghc\-prim\-0.6.1:GHC.Prim.RealWorld
**\->** (# ghc\-prim\-0.6.1:GHC.Prim.State#
ghc\-prim\-0.6.1:GHC.Prim.RealWorld,
a #))
*\-- Defined in ‘ghc-prim-0.6.1:GHC.Types’*
**instance** Applicative IO *\-- Defined in ‘GHC.Base’*
**instance** Functor IO *\-- Defined in ‘GHC.Base’*
**instance** Monad IO *\-- Defined in ‘GHC.Base’*
**instance** Monoid a **\=>** Monoid (IO a) *\-- Defined in ‘GHC.Base’*
**instance** Semigroup a **\=>** Semigroup (IO a) *\-- Defined in ‘GHC.Base’*
**instance** MonadFail IO *\-- Defined in ‘Control.Monad.Fail’*

Chúng ta thấy cá thể Monad đáng sợ , nhưng chúng ta cũng thấy một cá thể Functor . Functor là một loại lớp rất quan trọng trong Haskell. Nếu chúng ta nhìn vào nó trong REPL:

Prelude Week04.Contract\> :i Functor
**type** Functor **::** (\* **\->** \*) **\->** Constraint
**class** Functor f **where**
fmap **::** (a **\->** b) **\->** f a **\->** f b
(<$) **::** a **\->** f b **\->** f a
*{-# MINIMAL fmap #-}*
*\-- Defined in ‘GHC.Base’*
**instance** Functor (Either a) *\-- Defined in ‘Data.Either’*
**instance** Functor \[\] *\-- Defined in ‘GHC.Base’*
**instance** Functor Maybe *\-- Defined in ‘GHC.Base’*
**instance** Functor IO *\-- Defined in ‘GHC.Base’*
**instance** Functor ((**\->**) r) *\-- Defined in ‘GHC.Base’*
**instance** Functor ((,,,) a b c) *\-- Defined in ‘GHC.Base’*
**instance** Functor ((,,) a b) *\-- Defined in ‘GHC.Base’*
**instance** Functor ((,) a) *\-- Defined in ‘GHC.Base’*

Phương pháp quan trọng ở đây là fmap . Hàm thứ hai (<$) là một hàm tiện lợi.

fmap **::** (a **\->** b) **\->** f a **\->** f b

Hàm này, fmap , mà tất cả Functor có cho chúng ta biết rằng, nếu chúng ta cấp cho nó quyền truy cập vào một hàm có thể biến a thành b , thì nó có thể biến fa thành fb cho chúng ta. Ở đây, chúng ta quan tâm đến trường hợp f là IO .

Nếu chúng ta chuyên biệt hóa hàm cho IO , chúng ta sẽ có một hàm như:

fmap' **::** (a **\->** b) **\->** IO a **\->** IO b

Làm thế nào để làm việc đó. À, IO a là một công thức có tác dụng phụ và tạo ra a . Vì vậy, làm thế nào để chúng ta có được một b trong số đó? Chúng tôi thực hiện công thức, nhưng, trước khi trả về a , chúng tôi áp dụng hàm (a -> b) cho a và trả về kết quả là b .

Trong REPL, chúng ta hãy xem xét hàm toUpper .

Prelude Week04.Contract\> **import** **Data.Char**
Prelude Data.Char Week04.Contract\> :t toUpper
toUpper **::** Char **\->** Char
Prelude Data.Char Week04.Contract\> toUpper 'q'
'Q'

Nếu chúng ta muốn áp dụng điều đó cho một Chuỗi thay vì một Biểu đồ , chúng ta có thể sử dụng hàm bản đồ . Các chuỗi trong Haskell chỉ là danh sách các Char .

Prelude Data.Char Week04.Contract\> map toUpper "Haskell"
"HASKELL"

Hàm map toUpper là một hàm chuyển từ Chuỗi sang Chuỗi .

Prelude Data.Char Week04.Contract\> :t map toUpper
map toUpper **::** \[Char\] **\->** \[Char\]

Và chúng ta có thể sử dụng nó kết hợp với fmap . Nếu chúng ta sử dụng map toUpper làm hàm để chuyển đổi a thành b , chúng ta có thể thấy loại đầu ra của fmap sẽ như thế nào khi áp dụng cho IO a .

Prelude Data.Char Week04.Contract\> :t fmap (map toUpper) getLine
fmap (map toUpper) getLine **::** IO \[Char\]

Hãy xem nó trong hành động.

Prelude Data.Char Week04.Contract\> fmap (map toUpper) getLine
haskell
"HASKELL"

Chúng ta cũng có thể sử dụng toán tử >> . Điều này chuỗi hai hành động IO với nhau, bỏ qua kết quả của hành động đầu tiên. Trong ví dụ sau, cả hai hành động sẽ được thực hiện theo trình tự.

Prelude Week04.Contract\> putStrLn "Hello" \>> putStrLn "World"
Hello
World

Ở đây, không có kết quả từ putStrLn , nhưng nếu có, nó sẽ bị bỏ qua. Các tác dụng phụ của nó sẽ được thực hiện, kết quả của nó bị bỏ qua, sau đó tác dụng phụ putStrLn thứ hai sẽ được thực hiện trước khi trả về kết quả của lần gọi thứ hai.

Sau đó, có một toán tử quan trọng không bỏ qua kết quả của hành động IO đầu tiên , và đó được gọi là ràng buộc . Nó được viết dưới dạng ký hiệu >> = .

Prelude Week04.Contract\> :t (\>>=)
(\>>=) **::** Monad m **\=>** m a **\->** (a **\->** m b) **\->** m b

Chúng tôi thấy ràng buộc Monad , nhưng chúng tôi có thể bỏ qua điều đó ngay bây giờ và chỉ nghĩ về IO .

Điều này nói lên rằng nếu tôi có một công thức thực hiện các tác dụng phụ sau đó cho tôi kết quả a , và cho rằng tôi có một hàm nhận a và trả lại cho tôi một công thức trả về b , thì tôi có thể kết hợp công thức ma . với công thức mb bằng cách lấy giá trị a và sử dụng nó trong công thức thu được giá trị b .

Một ví dụ sẽ làm rõ điều này.

Prelude Week04.Contract\> getLine \>>= putStrLn
Haskell
Haskell

Ở đây, hàm getLine có kiểu IO String . Giá trị trả về a được chuyển cho hàm (a -> mb) , sau đó tạo ra một công thức putStrLn với giá trị đầu vào là a và đầu ra là kiểu IO () . Sau đó, putStrLn thực hiện các tác dụng phụ của nó và trả về Unit .

Có một cách khác, rất quan trọng, để tạo các hành động IO và đó là tạo các công thức nấu ăn ngay lập tức trả về kết quả mà không thực hiện bất kỳ tác dụng phụ nào.

Điều đó được thực hiện với một hàm được gọi là return .

Prelude Week04.Contract\> :t return
return **::** Monad m **\=>** a **\->** m a

Một lần nữa, nó là chung cho bất kỳ Monad nào, chúng ta chỉ cần nghĩ về IO ngay bây giờ.

Nó nhận một giá trị a và trả về một công thức tạo ra giá trị a . Trong trường hợp trả lại , công thức thực sự không tạo ra bất kỳ tác dụng phụ nào.

Ví dụ:

Prelude Week04.Contract\> return "Haskell" **::** IO String
"Haskell"

Chúng tôi cần chỉ định kiểu trả về để REPL biết chúng tôi đang sử dụng Monad nào:

Prelude Week04.Contract\> :t return "Haskell" **::** IO String
return "Haskell" **::** IO String **::** IO String

Prelude Week04.Contract\> :t return "Haskell"
return "Haskell" **::** Monad m **\=>** m \[Char\]

Nếu bây giờ chúng ta quay lại chương trình chính của mình, bây giờ chúng ta có thể viết các hành động **IO tương đối phức tạp . Ví dụ, chúng ta có thể xác định một hành động IO sẽ yêu cầu hai chuỗi và in kết quả của việc nối hai chuỗi đó với bảng điều khiển.

main **::** IO ()
main **\=** bar

bar **::** IO ()
bar **\=** getLine \>>= \\s **\->**
getLine \>>= \\t **\->**
putStrLn (s ++ t)

Và sau đó, khi chúng ta chạy nó, chương trình sẽ đợi hai đầu vào và sau đó xuất ra kết quả được nối.

cabal run hello
one
two
onetwo

Bây giờ điều này là đủ cho các mục đích của chúng tôi, mặc dù chúng tôi sẽ không cần IO Monad cho đến khi có lẽ sau này trong khóa học khi chúng tôi nói về việc thực sự triển khai các hợp đồng Plutus. Tuy nhiên, IO Monad là một ví dụ quan trọng và là một ví dụ tốt để bắt đầu.

Vì vậy, hiện tại, chúng ta hãy hoàn toàn quên IO và chỉ viết Haskell thuần túy, có chức năng, sử dụng loại Có thể .

Loại Có thể là một trong những loại hữu ích nhất trong Haskell.

Prelude Week04.Contract\> :i Maybe
**type** Maybe **::** \* **\->** \*
**data** Maybe a **\=** Nothing | Just a
*\-- Defined in ‘GHC.Maybe’*
**instance** Applicative Maybe *\-- Defined in ‘GHC.Base’*
**instance** Eq a **\=>** Eq (Maybe a) *\-- Defined in ‘GHC.Maybe’*
**instance** Functor Maybe *\-- Defined in ‘GHC.Base’*
**instance** Monad Maybe *\-- Defined in ‘GHC.Base’*
**instance** Semigroup a **\=>** Monoid (Maybe a) *\-- Defined in ‘GHC.Base’*
**instance** Ord a **\=>** Ord (Maybe a) *\-- Defined in ‘GHC.Maybe’*
**instance** Semigroup a **\=>** Semigroup (Maybe a)
*\-- Defined in ‘GHC.Base’*
**instance** Show a **\=>** Show (Maybe a) *\-- Defined in ‘GHC.Show’*
**instance** Read a **\=>** Read (Maybe a) *\-- Defined in ‘GHC.Read’*
**instance** Foldable Maybe *\-- Defined in ‘Data.Foldable’*
**instance** Traversable Maybe *\-- Defined in ‘Data.Traversable’*
**instance** MonadFail Maybe *\-- Defined in ‘Control.Monad.Fail’*

Nó thường được gọi là tùy chọn trong các ngôn ngữ lập trình khác.

Nó có hai hàm tạo - Không có gì , không lấy đối số và Chỉ , có một đối số.

**data** Maybe a **\=** Nothing | Just a

Hãy xem một ví dụ.

Trong Haskell, nếu bạn muốn truyền một Chuỗi đến một giá trị có một cá thể đọc , thông thường bạn sẽ thực hiện việc này với hàm đọc .

Week04.Maybe\> read "42" **::** Int
42

Tuy nhiên, đọc hơi khó chịu, bởi vì nếu chúng ta có thứ gì đó không thể phân tích cú pháp thành Int , thì chúng ta sẽ gặp lỗi.

Week04.Maybe\> read "42+u" **::** Int
\*\*\* Exception: Prelude.read: no parse

Hãy nhập readMaybe để làm điều đó theo cách tốt hơn.

Prelude Week04.Maybe\> **import** **Text.Read** (readMaybe)
Prelude Text.Read Week04.Contract\>

Hàm readMaybe thực hiện tương tự như hàm read , nhưng nó trả về giá trị Có thể và trong trường hợp không thể phân tích cú pháp, nó sẽ trả về giá trị Có thể được tạo bằng phương thức khởi tạo Không có  .

Prelude Text.Read Week04.Contract\> readMaybe "42" **::** Maybe Int
Just 42

Prelude Text.Read Week04.Contract\> readMaybe "42+u" **::** Maybe Int
Nothing

Giả sử chúng ta muốn tạo một hàm mới trả về giá trị Có thể .

foo :: String \-> String \-> String \-> Maybe Int

Ý tưởng là hàm nên cố gắng phân tích cú pháp cả ba Chuỗi thành Int . Nếu tất cả các chuỗi có thể được phân tích cú pháp thành công dưới dạng Int , thì chúng ta muốn cộng ba Int đó để nhận được một tổng. Nếu một trong các phân tích cú pháp không thành công, chúng tôi muốn trả về Không có  .

Một cách để làm điều đó sẽ là:

foo **::** String **\->** String **\->** String **\->** Maybe Int
foo x y z **\=** **case** readMaybe x **of**
Nothing **\->** Nothing
Just k **\->** **case** readMaybe y **of**
Nothing **\->** Nothing
Just l **\->** **case** readMaybe z **of**
Nothing **\->** Nothing
Just m **\->** Just (k + l + m)

Hãy xem nếu nó hoạt động. Đầu tiên, trường hợp thành công:

Prelude Week04.Contract\> :l Week04.Maybe
Prelude Week04.Maybe\> foo "1" "2" "3"
Just 6

Tuy nhiên, nếu một trong các giá trị không thể được phân tích cú pháp, chúng tôi sẽ không nhận được Không có  :

Prelude Week04.Maybe\> foo "" "2" "3"
Nothing

Mã này không lý tưởng vì chúng ta lặp lại cùng một mẫu ba lần. Mỗi lần chúng ta phải xem xét hai trường hợp - kết quả của bài đọc là Chỉ hay Không .

Hãy hỏi Haskellers, chúng tôi ghét sự lặp lại như thế này.

Điều chúng tôi muốn làm rất đơn giản. Chúng tôi muốn truyền ba chuỗi và thêm kết quả, nhưng với tất cả các trường hợp đó, nó rất ồn và rất xấu. Chúng tôi muốn loại bỏ mô hình này.

Một cách để làm điều đó là xác định một cái gì đó như:

bindMaybe **::** Maybe a **\->** (a **\->** Maybe b) **\->** Maybe b
bindMaybe Nothing **\=** Nothing
bindMaybe (Just x) f **\=** f x

Hãy viết lại cùng một hàm bằng bindMaybe .

foo' **::** String **\->** String **\->** String **\->** Maybe Int
foo' x y z **\=** readMaybe x \`bindMaybe\` \\k **\->**
readMaybe y \`bindMaybe\` \\l **\->**
readMaybe z \`bindMaybe\` \\m **\->**
Just (k + l + m)

Và sau đó, trong REPL, chúng tôi nhận được kết quả tương tự cho foo ' như chúng tôi nhận cho foo .

Prelude Week04.Maybe\> foo "1" "2" "3"
Just 6

Prelude Week04.Maybe\> foo "" "2" "3"
Nothing

Điều này thực hiện chính xác như foo , nhưng nó nhỏ gọn hơn nhiều, ít tiếng ồn hơn và logic kinh doanh rõ ràng hơn nhiều.

Nó có thể, hoặc có thể không, giúp xem chức năng mà nó không được sử dụng với ký hiệu infix:

Prelude Text.Read Week04.Maybe\> bindMaybe (readMaybe "42" **::** Maybe Int) (\\x **\->** Just x)
Just 42

Ở đây bạn có thể thấy rõ ràng hàm lấy có thể và sau đó là hàm lấy a từ Có thể và sử dụng nó làm đầu vào cho một hàm trả về Có thể mới .

Điều này tạo ra không có gì hữu ích, cho đến khi chúng tôi thêm readMaybe thứ hai

Prelude Text.Read Week04.Maybe\> bindMaybe (readMaybe "42" **::** Maybe Int) (\\x **\->** bindMaybe (readMaybe "5" **::** Maybe Int) (\\y **\->** Just (y + x)))
Just 47

Theo một số cách , Không có  là một ngoại lệ trong các ngôn ngữ khác. Nếu bất kỳ phép tính nào trả về Không có  , phần còn lại của các tính toán trong khối sẽ không được thực hiện và Không có gì được trả về.

Một kiểu rất hữu ích khác trong Haskell là kiểu Either .

Prelude Week04.Contract\> :i Either
**type** Either **::** \* **\->** \* **\->** \*
**data** Either a b **\=** Left a | Right b
*\-- Defined in ‘Data.Either’*
**instance** Applicative (Either e) *\-- Defined in ‘Data.Either’*
**instance** (Eq a, Eq b) **\=>** Eq (Either a b)
*\-- Defined in ‘Data.Either’*
**instance** Functor (Either a) *\-- Defined in ‘Data.Either’*
**instance** Monad (Either e) *\-- Defined in ‘Data.Either’*
**instance** (Ord a, Ord b) **\=>** Ord (Either a b)
*\-- Defined in ‘Data.Either’*
**instance** Semigroup (Either a b) *\-- Defined in ‘Data.Either’*
**instance** (Show a, Show b) **\=>** Show (Either a b)
*\-- Defined in ‘Data.Either’*
**instance** (Read a, Read b) **\=>** Read (Either a b)
*\-- Defined in ‘Data.Either’*
**instance** Foldable (Either a) *\-- Defined in ‘Data.Foldable’*
**instance** Traversable (Either a) *\-- Defined in ‘Data.Traversable’*

Hoặc nhận hai tham số, a và b . Giống như Có thể nó có hai hàm tạo, nhưng không giống như Có thể cả hai đều nhận một giá trị. Nó có thể là a hoặc a **b . Hai hàm tạo là Left và Right .

Ví dụ:

Prelude Week04.Contract\> Left "Haskell" **::** Either String Int
Left "Haskell"

Hoặc

Prelude Week04.Contract\> Right 7 **::** Either String Int
Right 7

Nếu chúng ta xem xét phép loại suy ngoại lệ xa hơn một chút, thì một vấn đề với Có thể là nếu chúng ta trả về Không có  , thì sẽ không có thông báo lỗi. Tuy nhiên, nếu chúng ta muốn một thứ gì đó mang lại thông điệp, chúng ta có thể thay thế Có thể bằng loại Một trong hai .

Trong trường hợp đó, Right có thể tương ứng với Just và Left có thể tương ứng với một lỗi, như Không có gì đã làm. Tuy nhiên, tùy thuộc vào loại mà chúng ta chọn cho a , chúng ta có thể đưa ra các thông báo lỗi thích hợp.

Hãy định nghĩa một thứ gọi là readEither và xem nó làm gì khi có thể và khi nào nó không thể phân tích cú pháp đầu vào của nó.

readEither **::** Read a **\=>** String **\->** Either String a
readEither s **case** readMaybe s **of**
Nothing **\->** Left $ "can't parse: " ++ s
Just a **\->** Right a

Prelude Week04.Either\> readEither "42" **::** Either String Int
Right 42

Prelude Week04.Either\> readEither "42+u" **::** Either String Int
Left "can't parse: 42+u"

Sử dụng điều này, bây giờ chúng ta có thể viết lại foo theo nghĩa của Either . Đầu tiên, sử dụng phương pháp dài dòng:

foo **::** String **\->** String **\->** String **\->** Either String Int
foo x y z **\=** **case** readEither x **of**
Left err **\->** Left err
Right k **\->** **case** readEither y **of**
Left err **\->** Left err
Right l **\->** **case** readEither z **of**
Left err **\->** Left err
Right m **\->** Right (k + l + m)

Hãy thử nó. Đầu tiên, con đường hạnh phúc:

Prelude Week04.Either\> foo "1" "2" "3"
Right 6

Sau đó, khi chúng tôi gặp sự cố:

Prelude Week04.Either\> foo "ays" "2" "3"
Left "can't parse: ays"

Nhưng, chúng tôi có cùng một vấn đề mà chúng tôi đã gặp phải với Có thể ; chúng tôi có rất nhiều sự lặp lại.

Giải pháp cũng tương tự.

bindEither **::** Either String a **\->** (a **\->** Either String b) **\->** Either String b
bindEither (Left err) **\_** **\=** Left err
bindEither (Right x) f **\=** f x

foo' **::** String **\->** String **\->** String **\->** Either String Int
foo' x y z **\=** readEither x \`bindEither\` \\k **\->**
readEither y \`bindEither\` \\l **\->**
readEither z \`bindEither\` \\m **\->**
Right (k + l + m)

Bạn có thể chạy lại điều này trong REPL và nó sẽ hoạt động giống như phiên bản dài dòng của nó.

Cho đến nay chúng ta đã xem xét ba ví dụ: IO a , Có thể là a và hoặc là Chuỗi a . IO a đại diện cho các kế hoạch có thể liên quan đến các tác dụng phụ và khi được thực hiện, tạo ra một a . Có thể một và một trong hai Chuỗi là một phép tính đại diện cho phép tính có thể tạo ra một nhưng cũng có thể thất bại. Sự khác biệt giữa Có thể và Hoặc là có thể không tạo ra bất kỳ thông báo lỗi nào, nhưng Có thể thì có.

Bây giờ chúng ta hãy xem xét một ví dụ hoàn toàn khác ghi lại ý tưởng về các phép tính cũng có thể tạo ra đầu ra nhật ký.

Chúng ta có thể thể hiện điều đó bằng một kiểu.

**data** Writer a **\=** Writer a \[String\]
**deriving** Show

Ví dụ, hãy viết một hàm trả về Writer cho một Int và viết một thông báo nhật ký.

number **::** Int **\->** Writer Int
number n **\=** Writer n $ \["number: " ++ show n\]

Trong REPL:

Prelude Week04.Writer\> number 42
Writer 42 \["number: 42"\]

Bây giờ, hãy làm điều gì đó tương tự như chúng ta đã làm với Có thể và Hoặc .

Hãy viết một hàm sử dụng ba phép tính ghi nhật ký mà mỗi phép tính tạo ra một Int và chúng ta muốn trả về một phép tính duy nhất tạo ra tổng của các Int đó.

foo **::** Writer Int **\->** Writer Int **\->** Writer Int **\->** Writer Int
foo (Writer k xs) (Writer l ys) (Writer m zs) **\=**
Writer (K + l + m) $ xs ++ ys ++ zs

Trong REPL:

Prelude Week04.Writer\> foo (number 1) (number 2) (number 3)
Writer 6 \["number: 1","number: 2","number: 3"\]

Bây giờ, chúng ta hãy viết một hàm hữu ích khác lấy một danh sách tin nhắn và tạo ra một Writer không có kết quả hữu ích.

tell **::** \[String\] **\->** Writer ()
tell **\=** Writer ()

Bây giờ, chúng tôi có thể cập nhật foo để thêm một thông báo nhật ký bổ sung hiển thị tổng các số.

foo **::** Writer Int **\->** Writer Int **\->** Writer Int **\->** Writer Int
foo (Writer k xs) (Writer l ys) (Writer m zs) **\=**
**let**
s **\=** k + l + m
Writer **\_** us **\=** tell \["sum: " ++ show s\]
**in**
Writer s $ xs ++ ys ++ zs ++ us

Trong REPL:

Prelude Week04.Writer\> foo (number 1) (number 2) (number 3)
Writer 6 \["number: 1","number: 2","number: 3","sum: 6"\]

Như trước đây, chúng ta có thể viết một hàm ràng buộc:

bindWriter **::** Writer a **\->** (a **\->** Writer b) **\->** Writer b
bindWriter (Writer a xs) f **\=**
**let**
Writer b ys **\=** f a
**in**
Writer b $ xs ++ ys

Ở đây, hàm bindWriter đang trả về Writer b và tạo ra các thông báo nhật ký là sự kết hợp của các xs mà chúng ta tạo mẫu đã khớp trên đầu vào và các y mà chúng ta tạo mẫu đã khớp khi gọi fa để tạo ra Writer b .

Bây giờ, chúng ta có thể viết lại foo bằng bindWriter và làm cho nó đẹp hơn nhiều.

foo' **::** Writer Int **\->** Writer Int **\->** Writer Int **\->** Writer Int
foo' x y z **\=** x \`bindWriter\` \\k **\->**
y \`bindWriter\` \\l **\->**
z \`bindWriter\` \\m **\->**
**let** s **\=** k + l + m
**in** tell \["sum: " ++ show s\] \`bindWriter\` \\**\_** **\->**
Writer s \[\]

Những gì chúng tôi đã làm với foo trước đây, bây giờ chúng tôi có thể làm với foo ' , và chúng tôi nhận được kết quả tương tự.

Prelude Week04.Writer\> foo' (number 1) (number 2) (number 3)
Writer 6 \["number: 1","number: 2","number: 3","sum: 6"\]

Phải thừa nhận rằng nó dài hơn so với trước đây, nhưng nó đã đẹp hơn rất nhiều. Chúng tôi không còn cần thực hiện đối sánh mẫu để trích xuất các thông báo. Chúng tôi không cần phải kết hợp các thông báo nhật ký một cách rõ ràng, nơi chúng tôi có thể mắc lỗi và quên một hoặc sai thứ tự. Thay vào đó, chúng tôi trừu tượng hóa tất cả những thứ đó đi và chỉ có thể tập trung vào logic kinh doanh.

Mặc dù mô hình giống như với Có thể và Hoặc , lưu ý rằng khía cạnh đặc biệt của các tính toán này là hoàn toàn khác nhau. Với Có thể và Hoặc , chúng tôi xử lý khái niệm thất bại, trong khi ở đây, với Người viết , không có thất bại, mà thay vào đó chúng tôi có thêm đầu ra.

Bây giờ, chúng ta đang ở trong một vị trí để giải thích Monad là gì.

Nhìn lại bốn ví dụ, chúng có điểm gì chung? Trong tất cả bốn trường hợp, Chúng tôi có một phương thức khởi tạo kiểu với một tham số kiểu - IO , Có thể , hoặc String và Writer đều nhận một tham số kiểu.

Và, đối với tất cả bốn ví dụ này, chúng tôi có một hàm ràng buộc. Đối với IO , chúng tôi có hàm >> = và đối với những người khác, chúng tôi có các hàm ràng buộc do chúng tôi tự viết.

bindWriter **::** Writer a **\->** (a **\->** Writer b) **\->** Writer b
bindEither **::** Either String a **\->** (a **\->** Either String b) **\->** Either String b
bindMaybe **::** Maybe a **\->** (a **\->** Maybe b) **\->** Maybe b

Cách thức hoạt động của ràng buộc tùy thuộc vào từng trường hợp. Trong trường hợp IO , nó là phép thuật tích hợp sẵn, nhưng bạn có thể nghĩ nó chỉ là kết hợp hai kế hoạch mô tả các hành động cần thực hiện trong quá trình tính toán. Đối với bindMaybe và bindE hoặc logic là cho toàn bộ kế hoạch sẽ thất bại nếu bất kỳ phần nào của nó không thành công và đối với bindWriter , logic là kết hợp danh sách các thông báo nhật ký.

Và đó là ý tưởng chính của Monads. Đó là một khái niệm về tính toán với một số tác dụng phụ bổ sung và khả năng liên kết hai tính toán đó lại với nhau.

Có một khía cạnh khác mà chúng tôi đã đề cập ngắn gọn trong trường hợp IO nhưng không phải đối với các ví dụ khác - một điều khác mà chúng tôi luôn có thể làm.

Bất cứ khi nào chúng ta có khái niệm tính toán với các tác dụng phụ như vậy, chúng ta cũng luôn có khả năng tạo ra một phép tính kiểu này mà không có bất kỳ tác dụng phụ nào.

Trong ví dụ về IO , điều này được thực hiện với lợi nhuận . Với một a , bạn có thể tạo IO a là công thức luôn trả về đơn giản là a mà không có tác dụng phụ. Mỗi ví dụ khác cũng có khả năng này, như được hiển thị bên dưới.

return              **::** a **\->** IO a
Just **::** a **\->** Maybe a
Right **::** a **\->** Either String a
(\\a **\->** Writer a \[\]) **::** a **\->** Writer a

Và chính sự kết hợp của hai đặc điểm này đã xác định một Monad.

  • khả năng liên kết hai phép tính với nhau
  • khả năng xây dựng một phép tính từ một giá trị thuần túy mà không sử dụng bất kỳ tác dụng phụ tiềm ẩn nào

Nếu chúng ta xem trong REPL:

Prelude Week04.Contract\> :i Monad
**type** Monad **::** (\* **\->** \*) **\->** Constraint
**class** Applicative m **\=>** Monad m **where**
(\>>=) **::** m a **\->** (a **\->** m b) **\->** m b
(\>>) **::** m a **\->** m b **\->** m b
return **::** a **\->** m a
*{-# MINIMAL (>>=) #-}*
*\-- Defined in ‘GHC.Base’*
**instance** Monad (Either e) *\-- Defined in ‘Data.Either’*
**instance** Monad \[\] *\-- Defined in ‘GHC.Base’*
**instance** Monad Maybe *\-- Defined in ‘GHC.Base’*
**instance** Monad IO *\-- Defined in ‘GHC.Base’*
**instance** Monad ((**\->**) r) *\-- Defined in ‘GHC.Base’*
**instance** (Monoid a, Monoid b, Monoid c) **\=>** Monad ((,,,) a b c)
*\-- Defined in ‘GHC.Base’*
**instance** (Monoid a, Monoid b) **\=>** Monad ((,,) a b)
*\-- Defined in ‘GHC.Base’*
**instance** Monoid a **\=>** Monad ((,) a) *\-- Defined in ‘GHC.Base’*

Chúng tôi thấy chức năng ràng buộc

(\>>=) **::** m a **\->** (a **\->** m b) **\->** m b

Và hàm trả về nhận một giá trị thuần túy và biến nó thành một phép tính tiềm ẩn tác dụng phụ, nhưng không sử dụng chúng.

return **::** a **\->** m a

Hàm khác >> có thể dễ dàng được xác định theo nghĩa >> = , nhưng được cung cấp để thuận tiện.

(\>>) **::** m a **\->** m b **\->** m b

Những gì hàm này làm là loại bỏ kết quả của phép tính đầu tiên, vì vậy bạn có thể xác định nó theo nghĩa >> = bằng cách bỏ qua đối số của tham số hàm.

Có một tính toán kỹ thuật khác. Chúng tôi thấy rằng Monad có **Ứng dụng siêu hạng , vì vậy mọi Monad đều là Ứng viên .

Prelude Week04.Contract\> :i Applicative
**type** Applicative **::** (\* **\->** \*) **\->** Constraint
**class** Functor f **\=>** Applicative f **where**
pure **::** a **\->** f a
(<\*>) **::** f (a **\->** b) **\->** f a **\->** f b
GHC.Base.liftA2 **::** (a **\->** b **\->** c) **\->** f a **\->** f b **\->** f c
(\*>) **::** f a **\->** f b **\->** f b
(<\*) **::** f a **\->** f b **\->** f a
*{-# MINIMAL pure, ((<\*>) | liftA2) #-}*
*\-- Defined in ‘GHC.Base’*
**instance** Applicative (Either e) *\-- Defined in ‘Data.Either’*
**instance** Applicative \[\] *\-- Defined in ‘GHC.Base’*
**instance** Applicative Maybe *\-- Defined in ‘GHC.Base’*
**instance** Applicative IO *\-- Defined in ‘GHC.Base’*
**instance** Applicative ((**\->**) r) *\-- Defined in ‘GHC.Base’*
**instance** (Monoid a, Monoid b, Monoid c) **\=>**
Applicative ((,,,) a b c)
*\-- Defined in ‘GHC.Base’*
**instance** (Monoid a, Monoid b) **\=>** Applicative ((,,) a b)
*\-- Defined in ‘GHC.Base’*
**instance** Monoid a **\=>** Applicative ((,) a) *\-- Defined in ‘GHC.Base’*

Chúng tôi thấy nó có một loạt các chức năng, nhưng chúng tôi chỉ cần hai chức năng đầu tiên.

pure **::** a **\->** f a
(<\*>) **::** f (a **\->** b) **\->** f a **\->** f b

Hàm pure có cùng kiểu chữ ký với hàm return . Sau đó, có <*> (phát âm là 'ap') trông phức tạp hơn một chút. Nhưng, sự thật là, khi bạn đã return và >> = trong Monad, chúng ta có thể dễ dàng xác định cả thuần túy và <*>.

Chúng tôi thấy rằng Ứng dụng cũng có một Functor siêu lớp .

Prelude Week04.Contract\> :i Functor
**type** Functor **::** (\* **\->** \*) **\->** Constraint
**class** Functor f **where**
fmap **::** (a **\->** b) **\->** f a **\->** f b
(<$) **::** a **\->** f b **\->** f a
*{-# MINIMAL fmap #-}*
*\-- Defined in ‘GHC.Base’*
**instance** Functor (Either a) *\-- Defined in ‘Data.Either’*
**instance** Functor \[\] *\-- Defined in ‘GHC.Base’*
**instance** Functor Maybe *\-- Defined in ‘GHC.Base’*
**instance** Functor IO *\-- Defined in ‘GHC.Base’*
**instance** Functor ((**\->**) r) *\-- Defined in ‘GHC.Base’*
**instance** Functor ((,,,) a b c) *\-- Defined in ‘GHC.Base’*
**instance** Functor ((,,) a b) *\-- Defined in ‘GHC.Base’*
**instance** Functor ((,) a) *\-- Defined in ‘GHC.Base’*

Như chúng ta đã đề cập trong ngữ cảnh của IO , Functor có hàm fmap , cho một hàm từ a đến b sẽ biến fa thành fb .

Ví dụ nguyên mẫu cho fmap là danh sách mà fmap chỉ là bản đồ . Cho một hàm từ a đến b , bạn có thể tạo danh sách kiểu b từ danh sách kiểu a bằng cách áp dụng hàm ánh xạ cho từng phần tử của danh sách.

Một lần nữa, một khi bạn có return và >> = , rất dễ dàng để xác định fmap .

Vì vậy, bất cứ khi nào bạn muốn xác định Monad, bạn chỉ cần xác định return và >> = , và để làm cho trình biên dịch hài lòng và cung cấp các phiên bản cho Functor và Ứng dụng , luôn có một cách tiêu chuẩn để làm điều đó.

Chúng ta có thể làm điều này trong ví dụ về Writer .

**import** **Control.Monad**

**instance** Functor Writer **where**
fmap **\=** liftM

**instance** Applicative Writer **where**
pure **\=** return
(<\*>) **\=** ap

**instance** Monad Writer **where**
return a **\=** Writer a \[\]
(\>>=) **\=** bindWriter

Chúng ta không cần phải làm như vậy đối với Có thể , Hoặc hoặc IO bởi vì chúng đã là Monad được xác định bởi Khúc dạo đầu.

Nói chung, việc xác định một mẫu chung và đặt tên cho nó luôn hữu ích.

Nhưng, có lẽ lợi thế quan trọng nhất là có rất nhiều chức năng không quan tâm đến Đơn vị nào mà chúng ta đang xử lý - chúng sẽ hoạt động với tất cả Đơn vị.

Hãy tổng quát hóa ví dụ nơi chúng ta tính tổng của ba số nguyên. Chúng tôi sử dụng chữ cái trong ví dụ bên dưới vì những lý do sẽ trở nên rõ ràng trong giây lát.

threeInts **::** Monad m **\=>** m Int **\->** m Int **\->** m Int **\->** m Int
threeInts mx my mz **\=**
mx \>>= \\k **\->**
my \>>= \\l **\->**
mz \>>= \\m **\->**
**let** s **\=** k + l + m **in** return s

Bây giờ chúng ta có chức năng này, chúng ta có thể quay lại ví dụ Có thể và viết lại nó.

foo'' **::** String **\->** String **\->** String **\->** Maybe Int
foo'' x y z **\=** threeInts (readMaybe x) (readMaybe y) (readMaybe z)

Chúng ta có thể làm tương tự cho ví dụ Either .

foo'' **::** String **\->** String **\->** String **\->** Either String Int
foo'' x y z **\=** threeInts (readEither x) (readEither y) (readEither z)

Ví dụ về Writer không hoàn toàn giống nhau.

Nếu chúng tôi không hài lòng khi không có thông báo nhật ký cho tổng, nó rất đơn giản vì nó đã là một thể hiện của ThreeInts .

foo'' **::** Writer Int **\->** Writer Int **\->** Writer Int **\->** Writer Int
foo'' x y z **\=** threeInts

Tuy nhiên, nếu chúng ta muốn thông báo nhật ký cuối cùng, nó sẽ trở nên phức tạp hơn một chút.

foo'' **::** Writer Int **\->** Writer Int **\->** Writer Int **\->** Writer Int
foo'' x y z **\=** **do**
s **<-** threeInts x y z
tell \["sum: " ++ show s\]
return s

Nếu bạn nhìn vào mô-đun Control.Monad trong Haskell Prelude tiêu chuẩn, bạn sẽ thấy rằng có rất nhiều chức năng hữu ích mà bạn có thể sử dụng cho tất cả các Monad.

Một cách để nghĩ về Monad là tính toán với một siêu năng lực.

Trong trường hợp của IO , siêu sức mạnh sẽ có những tác dụng phụ trong thế giới thực. Trong trường hợp của Có thể , siêu sức mạnh đang có thể thất bại. Sức mạnh siêu việt của Either là không thành công với một thông báo lỗi. Và trong trường hợp của Writer , siêu sức mạnh là ghi nhật ký tin nhắn.

Có một câu nói trong cộng đồng Haskell rằng Haskell có dấu chấm phẩy quá tải. Giải thích cho điều này là trong nhiều ngôn ngữ lập trình mệnh lệnh, bạn có dấu chấm phẩy kết thúc bằng dấu chấm phẩy - mỗi câu lệnh được thực thi lần lượt, mỗi câu cách nhau bằng dấu chấm phẩy. Nhưng, chính xác dấu chấm phẩy có nghĩa là gì phụ thuộc vào ngôn ngữ. Ví dụ, có thể có một ngoại lệ, trong trường hợp đó, quá trình tính toán sẽ dừng lại và không tiếp tục với các dòng tiếp theo.

Theo một nghĩa nào đó, bind giống như một dấu chấm phẩy. Và điều thú vị về Haskell là nó là một dấu chấm phẩy có thể lập trình được. Chúng ta có thể nói logic là gì để kết hợp hai phép tính với nhau.

Mỗi Monad đi kèm với “dấu chấm phẩy” riêng.

Bởi vì mô hình này rất phổ biến và các phép tính đơn lẻ ở khắp nơi, có một ký hiệu đặc biệt cho điều này trong Haskell, được gọi là ký hiệu không .

Nó là đường cú pháp. Hãy viết lại ba Kiến sử dụng  hiệu.

threeInts' **::** Monad m **\=>** m Int **\->** m Int **\->** m Int **\->** m Int
threeInts' mx my mz **\=** **do**
k **<-** mx
l **<-** my
m **<-** mz
**let** s **\=** k + l + m
return s

Điều này thực hiện chính xác những điều tương tự như phiên bản không phải do , nhưng nó có ít tiếng ồn hơn.

Lưu ý rằng câu lệnh let không sử dụng một phần . Nó không cần phải bên trong một khối do .

Và đó là Monads. Còn rất nhiều điều để nói về chúng nhưng hy vọng bây giờ bạn đã biết được Monads là gì và chúng hoạt động như thế nào.

Thường thì bạn đang ở trong tình huống bạn muốn có nhiều hiệu ứng cùng một lúc - ví dụ: bạn có thể muốn thông báo ghi nhật ký  lỗi tùy chọn . Có nhiều cách để làm điều đó trong Haskell. Ví dụ, có Monad Transformers, nơi về cơ bản người ta có thể xây dựng các Monad tùy chỉnh với các tính năng mà bạn muốn.

Có những cách tiếp cận khác. Một được gọi là Hệ thống Hiệu ứng, có mục tiêu tương tự. Và đây tình cờ là thứ mà Plutus sử dụng cho các Môn phái quan trọng. Đặc biệt là Đơn vị liên hệ trong ví và Đơn vị theo dõi được sử dụng để kiểm tra mã Plutus.

Tin tốt là bạn không cần phải hiểu Hệ thống Hiệu ứng để làm việc với các Monad này. Bạn chỉ cần biết rằng bạn đang làm việc với Monad, và nó có siêu năng lực nào.

Bây giờ chúng ta đã thấy cách viết mã Monad, bằng cách sử dụng ràng buộc và trả về hoặc bằng cách sử dụng ký hiệu, chúng ta có thể xem một Monad rất quan trọng, đó là Monad hợp đồng, mà bạn có thể đã nhận thấy trong các ví dụ mã trước đó.

Monad hợp đồng xác định mã sẽ chạy trong ví, đây là phần ngoài chuỗi của Plutus.

Tuy nhiên, trước khi đi vào chi tiết, chúng ta sẽ nói về Monad thứ hai, Monad EmulatorTrace.

Bạn có thể đã tự hỏi liệu có cách nào để thực thi mã Plutus cho mục đích thử nghiệm mà không cần sử dụng Sân chơi Plutus hay không. Thực sự là có, và điều này được thực hiện bằng EmulatorTrace Monad.

Bạn có thể nghĩ về một chương trình trong Monad này giống như những gì chúng tôi thực hiện thủ công trong tab giả lập của sân chơi. Nghĩa là, chúng tôi xác định các điều kiện ban đầu, chúng tôi xác định các hành động chẳng hạn như ví nào gọi điểm cuối nào với các tham số nào và chúng tôi xác định khoảng thời gian chờ giữa các hành động.

Các định nghĩa liên quan nằm trong gói plutus-contract trong mô-đun Plutus.Trace.Emulator .

module Plutus.Trace.Emulator

Chức năng cơ bản nhất được gọi là runEmulatorTrace .

*\-- | Run an emulator trace to completion, returning a tuple of the final state*
*\-- of the emulator, the events, and any error, if any.*
runEmulatorTrace
**::** EmulatorConfig
**\->** EmulatorTrace ()
**\->** (\[EmulatorEvent\], Maybe EmulatorErr, EmulatorState)
runEmulatorTrace cfg trace **\=**
(\\(xs :> (y, z)) **\->** (xs, y, z))
$ run
$ runReader ((initialDist . \_initialChainState) cfg)
$ foldEmulatorStreamM (generalize list)
$ runEmulatorStream cfg trace

Nó có một cái gì đó gọi là EmulatorConfig và EmulatorTrace () , là một tính toán thuần túy mà không có tác dụng phụ trong thế giới thực. Nó là một chức năng thuần túy thực thi theo dõi trên một blockchain được mô phỏng và sau đó đưa ra kết quả là danh sách các * EmulatorEvent, có thể là lỗi, nếu có, và cuối cùng là * EmulatorState cuối cùng .

*EmulatorConfig* được định nghĩa trong một mô-đun khác trong cùng một gói:

**module** **Wallet.Emulator.Stream**

**data** EmulatorConfig **\=**
EmulatorConfig
{ \_initialChainState **::** InitialChainState *\-- ^ State of the blockchain at the beginning of the simulation. Can be given as a map of funds to wallets, or as a block of transactions.*
} **deriving** (Eq, Show)

**type** InitialChainState **\=** Either InitialDistribution Block

Chúng tôi thấy nó chỉ có một trường, thuộc loại InitialChainState và nó là InitialDistribution hoặc Block .

InitialDistribution được định nghĩa trong một mô-đun khác trong cùng một gói và nó là một loại từ đồng nghĩa với bản đồ các cặp giá trị khóa từ Wallet * s đến * Value * s, như bạn mong đợi. * Giá trị có thể là mã thông báo lovelace hoặc mã nguồn gốc.

**module** **Plutus.Contract.Trace**

**type** InitialDistribution **\=** Map Wallet Value

Trong cùng một mô-đun, chúng ta thấy một thứ gọi là defaultDist trả về phân phối mặc định cho tất cả các ví. Nó thực hiện điều này bằng cách chuyển 10 ví được xác định bởi allWallets sang defaultDistFor để lấy danh sách các ví.

*\-- | The wallets used in mockchain simulations by default. There are*
*\-- ten wallets because the emulator comes with ten private keys.*
allWallets **::** \[EM.Wallet\]
allWallets **\=** EM.Wallet <$> \[1 .. 10\]

defaultDist **::** InitialDistribution
defaultDist **\=** defaultDistFor allWallets

defaultDistFor **::** \[EM.Wallet\] **\->** InitialDistribution
defaultDistFor wallets **\=** Map.fromList $ zip wallets (repeat (Ada.lovelaceValueOf 100\_000\_000))

Chúng tôi có thể thử điều này trong REPL:

Prelude Week04.Contract\> **import** **Plutus.Trace.Emulator**
Prelude Plutus.Trace.Emulator Week04.Contract\> **import** **Plutus.Contract.Trace**
Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Week04.Contract\> defaultDist
fromList \[(Wallet 1,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 2,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 3,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 4,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 5,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 6,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 7,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 8,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 9,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 10,Value (Map \[(,Map \[("",100000000)\])\]))\]

Chúng ta có thể thấy rằng mỗi ví trong số 10 ví đã được phân phối ban đầu là 100.000.000 người yêu thích.

Chúng tôi cũng có thể lấy số dư cho một ví cụ thể:

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Week04.Contract\> defaultDistFor \[Wallet 1\]
fromList \[(Wallet 1,Value (Map \[(,Map \[("",100000000)\])\]))\]

Nếu bạn muốn các giá trị ban đầu khác nhau, nếu bạn muốn mã thông báo gốc, thì bạn phải chỉ định giá trị đó theo cách thủ công.

Hãy xem những gì chúng ta cần để chạy dấu vết đầu tiên của mình:

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Week04.Contract\> :t runEmulatorTrace
runEmulatorTrace
**::** EmulatorConfig
**\->** EmulatorTrace ()
**\->** (\[Wallet.Emulator.MultiAgent.EmulatorEvent\], Maybe EmulatorErr,
Wallet.Emulator.MultiAgent.EmulatorState)

Vì vậy, chúng ta cần một EmulatorConfig mà chúng ta biết là có một InitialChainState .

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Week04.Contract\> **import** **Wallet.Emulator.Stream**
Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Wallet.Emulator.Stream Week04.Contract\> :i InitialChainState
**type** InitialChainState **::** \*
**type** InitialChainState **\=**
Either InitialDistribution Ledger.Blockchain.Block
*\-- Defined in ‘Wallet.Emulator.Stream’*

Nếu chúng ta lấy bên trái của defaultDist sẽ nhận được InitialDistribution .

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Wallet.Emulator.Stream Week04.Contract\> :t Left defaultDist
Left defaultDist **::** Either InitialDistribution b

Sau đó, chúng ta có thể sử dụng để xây dựng EmulatorConfig .

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Wallet.Emulator.Stream Week04.Contract\> EmulatorConfig $ Left defaultDist
EmulatorConfig {\_initialChainState **\=** Left (fromList \[(Wallet 1,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 2,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 3,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 4,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 5,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 6,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 7,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 8,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 9,Value (Map \[(,Map \[("",100000000)\])\])),(Wallet 10,Value (Map \[(,Map \[("",100000000)\])\]))\])}

Vì vậy, hãy thử runEmulatorTrace . Nhớ lại rằng, cũng như EmulatorConfig , chúng ta cũng cần chuyển vào EmulatorTrace và cái đơn giản nhất chúng ta có thể tạo đơn giản là cái trả về Unit - return () .

runEmulatorTrace (EmulatorConfig $ Left defaultDist) $ return ()

Nếu bạn chạy điều này trong REPL, bạn sẽ nhận được một lượng lớn dữ liệu xuất ra bảng điều khiển, mặc dù chúng tôi không làm gì với dấu vết. Nếu bạn muốn làm cho nó trở nên hữu ích, bằng cách nào đó, bạn phải lọc tất cả dữ liệu này thành một cái gì đó hợp lý và tổng hợp nó theo một cách nào đó.

May mắn thay, có các chức năng khác cũng như runEmulatorTrace . Một trong số đó là runEmulatorTraceIo chạy mô phỏng sau đó xuất ra dấu vết ở dạng đẹp trên màn hình.

runEmulatorTraceIO
**::** EmulatorTrace ()
**\->** IO ()
runEmulatorTraceIO **\=** runEmulatorTraceIO' def def

Để sử dụng chức năng này, chúng ta không cần chỉ định EmulatorConfig như chúng ta đã làm trước đây, vì theo mặc định sẽ chỉ sử dụng phân phối mặc định.

Trong REPL:

Prelude...> runEmulatorTraceIO $ return ()

Slot 00000: TxnValidate af5e6d25b5ecb26185289a03d50786b7ac4425b21849143ed7e18bcd70dc4db8
Slot 00000: SlotAdd Slot 1
Slot 00001: SlotAdd Slot 2
Final balances
Wallet 1:
{, ""}: 100000000
Wallet 2:
{, ""}: 100000000
Wallet 3:
{, ""}: 100000000
Wallet 4:
{, ""}: 100000000
Wallet 5:
{, ""}: 100000000
Wallet 6:
{, ""}: 100000000
Wallet 7:
{, ""}: 100000000
Wallet 8:
{, ""}: 100000000
Wallet 9:
{, ""}: 100000000
Wallet 10:
{, ""}: 100000000

Và chúng tôi thấy một đầu ra ngắn gọn, dễ quản lý hơn nhiều. Không có gì xảy ra, nhưng chúng tôi thấy giao dịch Genesis và sau đó là số dư cuối cùng cho mỗi ví.

Nếu bạn muốn kiểm soát nhiều hơn, cũng có runEmulatorTraceIO ' , sử dụng EmulatorConfig , vì vậy chúng tôi có thể chỉ định một phân phối khác.

runEmulatorTraceIO'
**::** TraceConfig
**\->** EmulatorConfig
**\->** EmulatorTrace ()
**\->** IO ()
runEmulatorTraceIO' tcfg cfg trace
**\=** runPrintEffect (outputHandle tcfg) $ runEmulatorTraceEff tcfg cfg trace

Nó cũng cần một TraceConfig , có hai trường.

**data** TraceConfig **\=** TraceConfig
{ showEvent **::** EmulatorEvent' **\->** Maybe String
*\-- ^ Function to decide how to print the particular events.*
, outputHandle **::** Handle
*\-- ^ Where to print the outputs to. Default: 'System.IO.stdout'*
}

Trường đầu tiên, showEvent là một hàm chỉ định EmulatorEvent * nào được hiển thị và cách chúng được hiển thị. Nó nhận một * EmulatorEvent làm đối số và có thể trả về Không có gì mà sự kiện sẽ không được hiển thị hoặc Chỉ với một chuỗi hiển thị cách sự kiện sẽ được hiển thị.

Đây là TraceConfig mặc định được runEmulatorTraceIO sử dụng . Chúng ta có thể thấy rằng hầu hết các sự kiện đều bị bỏ qua và chúng ta chỉ nhận được đầu ra cho một số sự kiện.

**instance** Default TraceConfig **where**
def **\=** TraceConfig
{ showEvent **\=** defaultShowEvent
, outputHandle **\=** stdout
}

defaultShowEvent **::** EmulatorEvent' **\->** Maybe String
defaultShowEvent **\=** \\**case**
UserThreadEvent (UserLog msg) **\->** Just $ "\*\*\* USER LOG: " <> msg
InstanceEvent (ContractInstanceLog (ContractLog (A.String msg)) **\_** **\_**) **\->** Just $ "\*\*\* CONTRACT LOG: " <> show msg
InstanceEvent (ContractInstanceLog (StoppedWithError err) **\_** **\_**) **\->** Just $ "\*\*\* CONTRACT STOPPED WITH ERROR: " <> show err
InstanceEvent (ContractInstanceLog NoRequestsHandled **\_** **\_**) **\->** Nothing
InstanceEvent (ContractInstanceLog (HandledRequest **\_**) **\_** **\_**) **\->** Nothing
InstanceEvent (ContractInstanceLog (CurrentRequests **\_**) **\_** **\_**) **\->** Nothing
SchedulerEvent **\_** **\->** Nothing
ChainIndexEvent **\_** **\_** **\->** Nothing
WalletEvent **\_** **\_** **\->** Nothing
ev **\->** Just . renderString . layoutPretty defaultLayoutOptions . pretty $ ev

Trường thứ hai là một xử lý được mặc định là stdout , nhưng chúng tôi cũng có thể chỉ định một tệp ở đây.

Bây giờ chúng ta hãy xem xét một dấu vết thú vị hơn, sử dụng hợp đồng Vesting từ bài giảng trước.

Đầu tiên, chúng tôi xác định một Dấu vết .

myTrace **::** EmulatorTrace ()
myTrace **\=** **do**
h1 **<-** activateContractWallet (Wallet 1) endpoints
h2 **<-** activateContractWallet (Wallet 2) endpoints
callEndpoint @"give" h1 $ GiveParams
{ gpBeneficiary **\=** pubKeyHash $ walletPubKey $ Wallet 2
, gpDeadline **\=** Slot 20
, gpAmount **\=** 1000
}
void $ waitUntilSlot 20
callEndpoint @"grab" h2 ()
void $ waitNSlots 1

Điều đầu tiên chúng ta phải làm là kích hoạt ví bằng cách sử dụng chức năng Monad activeContractWallet . Chúng tôi liên kết kết quả của hàm này với h1 , và sau đó liên kết kết quả của lần gọi thứ hai (cho Ví 2) với h2 . Hai giá trị đó - h1 và h2 được xử lý đối với ví tương ứng của chúng.

Tiếp theo, chúng tôi sử dụng callEndpoint để mô phỏng Ví 1 gọi điểm cuối cho , với các thông số được hiển thị. Sau đó, chúng tôi chờ đợi cho 20 khe. Hàm waitUntilSlot thực sự trả về một giá trị đại diện cho vị trí đã đạt đến, nhưng vì chúng tôi không quan tâm đến giá trị đó ở đây, chúng tôi sử dụng void để bỏ qua nó. Sau đó, chúng tôi mô phỏng cuộc gọi đến điểm cuối lấy bằng Ví 2.

Bây giờ, chúng ta có thể viết một hàm để gọi runEmulatorTraceIO mà không cần Trace .

test **::** IO ()
test **\=** runEmulatorTraceIO myTrace

Và, sau đó chúng ta có thể chạy điều này trong REPL:

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Wallet.Emulator Week04.Trace Wallet.Emulator.Stream Week04.Contract\> test

Slot 00000: TxnValidate af5e6d25b5ecb26185289a03d50786b7ac4425b21849143ed7e18bcd70dc4db8
Slot 00000: SlotAdd Slot 1
Slot 00001: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Contract instance started
Slot 00001: 00000000\-0000\-4000\-8000\-000000000001 {Contract instance **for** wallet 2}:
Contract instance started
Slot 00001: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Receive endpoint call: Object (fromList \[("tag",String "give"),("value",Object (fromList \[("unEndpointValue",Object (fromList \[("gpAmount",Number 1000.0),("gpBeneficiary",Object (fromList \[("getPubKeyHash",String "39f713d0a644253f04529421b9f51b9b08979d08295959c4f3990ee617f5139f")\])),("gpDeadline",Object (fromList \[("getSlot",Number 20.0)\]))\]))\]))\])
Slot 00001: W1: TxSubmit: 49f326a21c09ba52eddee46b65bdb5fb33b3444745e9af1510a68f9043696eba
Slot 00001: TxnValidate 49f326a21c09ba52eddee46b65bdb5fb33b3444745e9af1510a68f9043696eba
Slot 00001: SlotAdd Slot 2
Slot 00002: \*\*\* CONTRACT LOG: "made a gift of 1000 lovelace to 39f713d0a644253f04529421b9f51b9b08979d08295959c4f3990ee617f5139f with deadline Slot {getSlot = 20}"
Slot 00002: SlotAdd Slot 3
Slot 00003: SlotAdd Slot 4
Slot 00004: SlotAdd Slot 5
Slot 00005: SlotAdd Slot 6
Slot 00006: SlotAdd Slot 7
Slot 00007: SlotAdd Slot 8
Slot 00008: SlotAdd Slot 9
Slot 00009: SlotAdd Slot 10
Slot 00010: SlotAdd Slot 11
Slot 00011: SlotAdd Slot 12
Slot 00012: SlotAdd Slot 13
Slot 00013: SlotAdd Slot 14
Slot 00014: SlotAdd Slot 15
Slot 00015: SlotAdd Slot 16
Slot 00016: SlotAdd Slot 17
Slot 00017: SlotAdd Slot 18
Slot 00018: SlotAdd Slot 19
Slot 00019: SlotAdd Slot 20
Slot 00020: 00000000\-0000\-4000\-8000\-000000000001 {Contract instance **for** wallet 2}:
Receive endpoint call: Object (fromList \[("tag",String "grab"),("value",Object (fromList \[("unEndpointValue",Array \[\])\]))\])
Slot 00020: W2: TxSubmit: d9a2028384b4472242371f27cb51727f5c7c04327972e4278d1f69f606019a8b
Slot 00020: TxnValidate d9a2028384b4472242371f27cb51727f5c7c04327972e4278d1f69f606019a8b
Slot 00020: SlotAdd Slot 21
Slot 00021: \*\*\* CONTRACT LOG: "collected gifts"
Slot 00021: SlotAdd Slot 22
Final balances
Wallet 1:
{, ""}: 99998990
Wallet 2:
{, ""}: 100000990
Wallet 3:
{, ""}: 100000000
Wallet 4:
{, ""}: 100000000
Wallet 5:
{, ""}: 100000000
Wallet 6:
{, ""}: 100000000
Wallet 7:
{, ""}: 100000000
Wallet 8:
{, ""}: 100000000
Wallet 9:
{, ""}: 100000000
Wallet 10:
{, ""}: 100000000

Đầu ra này rất giống với đầu ra mà chúng ta thấy trong sân chơi. Chúng ta có thể thấy giao dịch Genesis cũng như cả giao dịch cho và lấy từ Trace . Chúng ta cũng có thể thấy một số đầu ra nhật ký từ chính hợp đồng, có tiền tố là NHẬT KÝ HỢP ĐỒNG .

Chúng tôi cũng có thể đăng nhập từ bên trong Monad Trace . Ví dụ, chúng tôi có thể xem kết quả của cuộc gọi waitNSlots cuối cùng :

myTrace **::** EmulatorTrace ()
myTrace **\=** **do**
...
...
s **<-** waitNSlots 1
Extras.logInfo $ "reached slot " ++ show s

Sau đó, chúng tôi sẽ thấy kết quả này khi chúng tôi chạy mô phỏng:

...
Slot 00020: SlotAdd Slot 21
Slot 00021: \*\*\* USER LOG: reached slot Slot {getSlot \= 21}
Slot 00021: \*\*\* CONTRACT LOG: "collected gifts"
Slot 00021: SlotAdd Slot 22
...

Bây giờ chúng ta hãy nhìn vào Monad Hợp đồng.

Mục đích của Monad hợp đồng là xác định mã ngoài chuỗi chạy trong ví. Nó có bốn tham số kiểu:

**newtype** Contract w s e a **\=** Contract { unContract **::** Eff (ContractEffs w s e) a }
**deriving** **newtype** (Functor, Applicative, Monad)

giống như trong mọi Monad - nó biểu thị kiểu kết quả của phép tính.

Chúng ta sẽ đi vào chi tiết hơn ba phần khác sau nhưng chỉ ngắn gọn:

  • w giống như ví dụ Monad Writer của chúng tôi, nó cho phép chúng tôi viết các thông báo nhật ký kiểu w .
  • s mô tả các khả năng của blockchain, ví dụ như đợi một vị trí, gửi giao dịch, lấy khóa công khai của ví. Nó cũng có thể chứa các điểm cuối cụ thể.
  • e mô tả loại thông báo lỗi mà Monad này có thể ném ra.

Hãy viết một ví dụ.

myContract1 **::** Contract () BlockchainActions Text ()
myContract1 **\=** Contract.logInfo @String "Hello from the contract!"

Ở đây, chúng tôi chuyển một Hợp đồng được xây dựng với Đơn vị là loại w và BlockchainActions làm đối số thứ hai, s . Điều này cho phép chúng tôi truy cập vào tất cả các hành động blockchain - điều duy nhất chúng tôi không thể làm là gọi các điểm cuối cụ thể.

Đối với e - loại thông báo lỗi, chúng tôi sử dụng Văn bản . Văn bản là một kiểu Haskell giống như Chuỗi , nhưng nó hiệu quả hơn nhiều.

Chúng tôi không muốn một kết quả cụ thể, vì vậy chúng tôi sử dụng Đơn vị cho loại a .

Đối với phần thân hàm, chúng tôi viết một thông báo nhật ký. Chúng tôi sử dụng @String bởi vì, chúng tôi đã nhập kiểu Data.Text và chúng tôi đã sử dụng tùy chọn trình biên dịch GHC OverloadedStrings , vì vậy trình biên dịch cần biết loại mà chúng tôi đang tham chiếu - Văn bản hay Chuỗi . Chúng ta có thể sử dụng @String nếu chúng ta cũng sử dụng tùy chọn trình biên dịch TypeApplication .

Bây giờ chúng ta hãy xác định một Dấu vết bắt đầu hợp đồng trong ví và một chức năng thử nghiệm để chạy nó.

myTrace1 **::** EmulatorTrace ()
myTrace1 **\=** void $ activateContractWallet (Wallet 1) myContract1

test1 **::** IO ()
test1 **\=** runEmulatorTraceIO myTrace1

Nếu chúng tôi chạy điều này trong REPL, chúng tôi sẽ thấy thông báo nhật ký của chúng tôi từ hợp đồng.

Bây giờ, hãy ném một ngoại lệ.

myContract1 **::** Contract () BlockchainActions Text ()
myContract1 **\=** **do**
void $ Contract.throwError "BOOM!"
Contract.logInfo @String "Hello from the contract!"

Nhớ lại rằng chúng tôi đã chọn loại Văn bản làm thông báo lỗi.

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Wallet.Emulator Week04.Trace Wallet.Emulator.Stream Week04.Contract\> test1
Slot 00000: TxnValidate af5e6d25b5ecb26185289a03d50786b7ac4425b21849143ed7e18bcd70dc4db8
Slot 00000: SlotAdd Slot 1
Slot 00001: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Contract instance started
Slot 00001: \*\*\* CONTRACT STOPPED WITH ERROR: "**\\"**BOOM!**\\"**"
Slot 00001: SlotAdd Slot 2
Final balances
Wallet 1:
{, ""}: 100000000
Wallet 2:
{, ""}: 100000000
Wallet 3:
{, ""}: 100000000
Wallet 4:
{, ""}: 100000000
Wallet 5:
{, ""}: 100000000
Wallet 6:
{, ""}: 100000000
Wallet 7:
{, ""}: 100000000
Wallet 8:
{, ""}: 100000000
Wallet 9:
{, ""}: 100000000
Wallet 10:
{, ""}: 100000000

Bây giờ, chúng tôi không nhận được thông báo nhật ký, nhưng chúng tôi được thông báo rằng hợp đồng đã dừng do lỗi và chúng tôi thấy thông báo ngoại lệ của mình.

Một điều khác bạn có thể làm là xử lý các trường hợp ngoại lệ. Chúng tôi sẽ sử dụng hàm handleError từ mô-đun Plutus.Contract.Types .

handleError **::**
forall w s e e' a.
(e **\->** Contract w s e' a)
**\->** Contract w s e a
**\->** Contract w s e' a
handleError f (Contract c) **\=** Contract c' **where**
c' **\=** E.handleError @e (raiseUnderN @'\[E.Error e'\] c) (fmap unContract f)

Hàm handleError nhận một trình xử lý lỗi và một cá thể Hợp đồng . Trình xử lý lỗi lấy một đối số kiểu e từ hợp đồng của chúng ta và trả về một Hợp đồng mới có cùng kiểu tham số với tham số đầu tiên, nhưng chúng ta có thể thay đổi kiểu của đối số e - kiểu lỗi, được thể hiện trong đối số Hợp đồng trả về liệt kê dưới dạng e ' .

myContract2 **::** Contract () BlockchainActions Void ()
myContract2 **\=** Contract.handleError
(\\err **\->** Contract.logError $ "Caught error: " ++ unpack err)
myContract1

myTrace2 **::** EmulatorTrace ()
myTrace2 **\=** void $ activateContractWallet (Wallet 1) myContract2

test2 **::** IO ()
test2 **\=** runEmulatorTraceIO myTrace2

Chúng tôi sử dụng kiểu Void làm kiểu lỗi. Void là một loại không có giá trị, vì vậy, bằng cách sử dụng loại này, chúng tôi muốn nói rằng không thể có bất kỳ sai sót nào đối với hợp đồng này.

Ghi chú

Chức năng giải nén được định nghĩa trong mô-đun Data.Text . Nó chuyển đổi giá trị kiểu Văn bản thành giá trị kiểu Chuỗi .

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Wallet.Emulator Week04.Trace Wallet.Emulator.Stream Week04.Contract\> test2
Slot 00000: TxnValidate af5e6d25b5ecb26185289a03d50786b7ac4425b21849143ed7e18bcd70dc4db8
Slot 00000: SlotAdd Slot 1
Slot 00001: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Contract instance started
Slot 00001: \*\*\* CONTRACT LOG: "Caught error: BOOM!"
Slot 00001: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Contract instance stopped (no errors)
Slot 00001: SlotAdd Slot 2
Final balances
...

Chúng tôi không còn nhận được thông báo lỗi nữa, nhưng thay vào đó, chúng tôi nhận được thông báo từ trình xử lý lỗi hiển thị ngoại lệ đã được đưa ra bởi Contract1. Lưu ý rằng chúng tôi vẫn không nhận được thông báo "Xin chào từ hợp đồng!". Hợp đồng 1 vẫn ngừng xử lý sau lỗi của nó, nhưng không có lỗi tổng thể của hợp đồng do ngoại lệ được phát hiện và xử lý.

Tất nhiên, các trường hợp ngoại lệ cũng có thể xảy ra ngay cả khi chúng không được mã hợp đồng của bạn đưa ra một cách rõ ràng. Có những hoạt động, chẳng hạn như gửi một giao dịch mà không có đủ đầu vào để thực hiện thanh toán cho một đầu ra, trong đó Plutus sẽ đưa ra một ngoại lệ.

Tiếp theo, hãy xem tham số s , tham số thứ hai cho Hợp đồng , xác định các hành động blockchain có sẵn.

Trong hai ví dụ đầu tiên, chúng tôi chỉ sử dụng kiểu BlockChainActions có tất cả các chức năng tiêu chuẩn nhưng không hỗ trợ cho các điểm cuối cụ thể. Nếu chúng tôi muốn hỗ trợ cho các điểm cuối cụ thể, chúng tôi phải sử dụng một loại khác.

Cách thường được thực hiện là sử dụng từ đồng nghĩa loại. Ví dụ sau sẽ tạo một kiểu từ đồng nghĩa MySchema có tất cả các khả năng của BlockChainActions nhưng có thêm khả năng gọi điểm cuối foo với đối số kiểu Int .

**type** MySchema **\=** BlockchainActions .\\/ Endpoint "foo" Int

Ghi chú

Toán tử . \ / Là một toán tử kiểu - nó hoạt động trên các kiểu, không phải giá trị. Để sử dụng điều này, chúng ta cần sử dụng các tùy chọn trình biên dịch TypeOperators và DataKinds .

Bây giờ, chúng ta có thể sử dụng kiểu MySchema để xác định hợp đồng của mình.

myContract3 **::** Contract () MySchema Text ()
myContract3 **\=** **do**
n **<-** endpoint @"foo"
Contract.logInfo n

Hợp đồng này sẽ chặn cho đến khi foo điểm cuối được gọi với, trong trường hợp của chúng tôi, là Int . Khi đó giá trị của tham số Int sẽ bị ràng buộc với n . Bởi vì điều này, không còn đủ để chúng tôi chỉ kích hoạt hợp đồng để kiểm tra nó. Bây giờ, chúng ta cũng phải gọi điểm cuối.

Để làm điều này, bây giờ chúng ta cần xử lý từ activeContractWallet , sau đó chúng ta có thể sử dụng để gọi điểm cuối.

myTrace3 **::** EmulatorTrace ()
myTrace3 **\=** **do**
h **<-** activateContractWallet (Wallet 1) myContract3
callEndpoint @"foo" h 42

test3 **::** IO ()
test3 **\=** runEmulatorTraceIO myTrace3

Chạy điều này trong REPL:

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Wallet.Emulator Week04.Trace Wallet.Emulator.Stream Week04.Contract\> test3
Slot 00000: TxnValidate af5e6d25b5ecb26185289a03d50786b7ac4425b21849143ed7e18bcd70dc4db8
...
Receive endpoint call: Object (fromList \[("tag",String "foo"),("value",Object (fromList \[("unEndpointValue",Number 42.0)\]))\])
Slot 00001: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Contract log: Number 42.0
...
Final balances
...
Wallet 10:
{, ""}: 100000000

Cuối cùng, hãy xem tham số kiểu đầu tiên, người viết. W không thể là một kiểu tùy ý, nó phải là một thể hiện của kiểu Monoid .

Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Wallet.Emulator Week04.Trace Wallet.Emulator.Stream Week04.Contract\> :i Monoid
**type** Monoid **::** \* **\->** Constraint
**class** Semigroup a **\=>** Monoid a **where**
mempty **::** a
mappend **::** a **\->** a **\->** a
mconcat **::** \[a\] **\->** a
*{-# MINIMAL mempty #-}*
*\-- Defined in ‘GHC.Base’*
**instance** Monoid \[a\] *\-- Defined in ‘GHC.Base’*
**instance** Monoid Ordering *\-- Defined in ‘GHC.Base’*
**instance** Semigroup a **\=>** Monoid (Maybe a) *\-- Defined in ‘GHC.Base’*
**instance** Monoid a **\=>** Monoid (IO a) *\-- Defined in ‘GHC.Base’*
**instance** Monoid b **\=>** Monoid (a **\->** b) *\-- Defined in ‘GHC.Base’*
**instance** (Monoid a, Monoid b, Monoid c, Monoid d, Monoid e) **\=>**
Monoid (a, b, c, d, e)
*\-- Defined in ‘GHC.Base’*
**instance** (Monoid a, Monoid b, Monoid c, Monoid d) **\=>**
Monoid (a, b, c, d)
*\-- Defined in ‘GHC.Base’*
**instance** (Monoid a, Monoid b, Monoid c) **\=>** Monoid (a, b, c)
*\-- Defined in ‘GHC.Base’*
**instance** (Monoid a, Monoid b) **\=>** Monoid (a, b)
*\-- Defined in ‘GHC.Base’*
**instance** Monoid () *\-- Defined in ‘GHC.Base’*

Đây là một lớp kiểu rất quan trọng và rất phổ biến trong Haskell. Nó định nghĩa mempty và mappend .

Hàm mempty giống như phần tử trung tính và mappend kết hợp hai phần tử của loại này để tạo ra một phần tử mới cùng loại.

Ví dụ chính của Monoid là List , khi mempty là danh sách trống [] và mappend là nối ++ .

Ví dụ:

Prelude\> mempty **::** \[Int\]
\[\]
Prelude\> mappend \[1, 2, 3 **::** Int\] \[4, 5, 6\]
\[1,2,3,4,5,6\]

Có rất nhiều ví dụ khác về kiểu Monoid , và chúng ta sẽ xem các trường hợp khác trong khóa học này.

Nhưng bây giờ, chúng ta hãy gắn bó với các danh sách và viết ví dụ cuối cùng của chúng ta.

    myContract4 **::** Contract \[Int\] BlockchainActions Text ()
myContract4 **\=** **do**
void $ Contract.waitNSlots 10
tell \[1\]
void $ Contract.waitNSlots 10
tell \[2\]
void $ Contract.waitNSlots 10

Thay vì sử dụng Đơn vị làm loại w của chúng tôi , chúng tôi đang sử dụng [Int] . Điều này cho phép chúng tôi sử dụng chức năng cho biết như được hiển thị.

Điều này bây giờ cho phép chúng tôi truy cập vào các thông báo đó trong quá trình theo dõi, bằng cách sử dụng chức năng ObservableState .

    myTrace4 **::** EmulatorTrace ()
myTrace4 **\=** **do**
h **<-** activateContractWallet (Wallet 1) myContract4

void $ Emulator.waitNSlots 5
xs **<-** observableState h
Extras.logInfo $ show xs

void $ Emulator.waitNSlots 10
ys **<-** observableState h
Extras.logInfo $ show ys

void $ Emulator.waitNSlots 10
zs **<-** observableState h
Extras.logInfo $ show zs

test4 **::** IO ()
test4 **\=** runEmulatorTraceIO myTrace4

Nếu chúng ta chạy điều này trong REPL, chúng ta có thể thấy các thông báo USER LOG được tạo bằng cách sử dụng chức năng nói .

    Prelude Plutus.Trace.Emulator Plutus.Contract.Trace Wallet.Emulator Week04.Trace Wallet.Emulator.Stream Week04.Contract\> test4
Slot 00000: TxnValidate af5e6d25b5ecb26185289a03d50786b7ac4425b21849143ed7e18bcd70dc4db8
Slot 00000: SlotAdd Slot 1
Slot 00001: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Contract instance started
Slot 00001: SlotAdd Slot 2
...
Slot 00005: SlotAdd Slot 6
Slot 00006: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Sending contract state to Thread 0
Slot 00006: SlotAdd Slot 7
Slot 00007: \*\*\* USER LOG: \[\]
Slot 00007: SlotAdd Slot 8
...
Slot 00015: SlotAdd Slot 16
Slot 00016: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Sending contract state to Thread 0
Slot 00016: SlotAdd Slot 17
Slot 00017: \*\*\* USER LOG: \[1\]
Slot 00017: SlotAdd Slot 18
...
Slot 00025: SlotAdd Slot 26
Slot 00026: 00000000\-0000\-4000\-8000\-000000000000 {Contract instance **for** wallet 1}:
Sending contract state to Thread 0
Slot 00026: SlotAdd Slot 27
Slot 00027: \*\*\* USER LOG: \[1,2\]
Final balances
Wallet 1:
{, ""}: 100000000
Wallet 2:
{, ""}: 100000000
...
Wallet 10:
{, ""}: 100000000

Sử dụng cơ chế này, có thể truyền thông tin từ hợp đồng đang chạy trong ví ra thế giới bên ngoài. Sử dụng thiết bị đầu cuối, chúng tôi có thể chuyển thông tin vào một hợp đồng. Và bằng cách sử dụng cơ chế thông báo , chúng tôi có thể lấy thông tin ra khỏi ví.