2012-06-06 25 views
10

Tôi đang cố viết một trình chơi Spider Solitaire như một bài tập học tập Haskell.Kết hợp các monads trong Haskell

chức năng My main sẽ gọi một hàm playGame một lần cho mỗi trò chơi (sử dụng mapM), đi qua trong số trò chơi và một máy phát ngẫu nhiên (StdGen). Hàm playGame phải trả về một đơn vị Control.Monad.State và một đơn nguyên IO có chứa một số String hiển thị tableau trò chơi và Bool cho biết trò chơi đã thắng hay thua.

Làm cách nào để kết hợp đơn lẻ State với đơn vị IO cho giá trị trả lại? Khai báo kiểu cho `playGame là gì?

playGame :: Int -> StdGen a -> State IO (String, Bool) 

State IO (String, Bool) có đúng không? Nếu không, nó nên là gì?

Trong main, tôi có kế hoạch sử dụng

do 
    -- get the number of games from the command line (already written) 
    results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames] 

Đây có phải là cách chính xác để gọi playGame?

+0

Bạn cũng có thể thưởng thức 'RandT' từ gói [MonadRandom] (http://hackage.haskell.org/package/MonadRandom). –

Trả lời

12

gì bạn muốn là StateT s IO (String, Bool), nơi StateT được cung cấp bởi cả hai Control.Monad.State (từ gói mtl) và Control.Monad.Trans.State (từ gói transformers).

Hiện tượng chung này được gọi là biến áp đơn nguyên và bạn có thể đọc phần giới thiệu tuyệt vời về chúng trong Monad Transformers, Step by Step.

Có hai cách để xác định chúng. Một trong số chúng được tìm thấy trong gói transformers sử dụng lớp MonadTrans để triển khai chúng. Cách tiếp cận thứ hai được tìm thấy trong lớp mtl và sử dụng một loại lớp riêng biệt cho mỗi đơn vị.

Ưu điểm của cách tiếp cận transformers là việc sử dụng một loại lớp duy nhất để thực hiện tất cả mọi thứ (tìm thấy here):

class MonadTrans t where 
    lift :: Monad m => m a -> t m a 

lift có hai thuộc tính tốt đẹp mà bất kỳ trường hợp MonadTrans phải đáp ứng:

(lift .) return = return 
(lift .) f >=> (lift .) g = (lift .) (f >=> g) 

Đây là các luật functor trong ngụy trang, trong đó (lift .) = fmap, return = id(>=>) = (.).

Cách tiếp cận mtl type-class có lợi ích của nó, quá, và một số điều chỉ được sạch sẽ có thể giải quyết bằng cách sử dụng mtl kiểu lớp, tuy nhiên nhược điểm là sau đó rằng mỗi mtl type-class đã thiết lập riêng của pháp luật bạn có để nhớ khi thực hiện các trường hợp cho nó. Ví dụ, loại hạng MonadError (tìm thấy here) được định nghĩa là:

class Monad m => MonadError e m | m -> e where 
    throwError :: e -> m a 
    catchError :: m a -> (e -> m a) -> m a 

lớp này đi kèm với pháp luật, quá:

m `catchError` throwError = m 
(throwError e) `catchError` f = f e 
(m `catchError` f) `catchError` g = m `catchError` (\e -> f e `catchError` g) 

Đây chỉ là những luật đơn nguyên trong ngụy trang, nơi throwError = returncatchError = (>>=) (và các luật đơn nguyên là luật danh mục trong ngụy trang, trong đó return = id(>=>) = (.)).

Đối với vấn đề cụ thể của bạn, cách bạn sẽ viết chương trình của bạn sẽ là như nhau:

do 
    -- get the number of games from the command line (already written) 
    results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames] 

... nhưng khi bạn viết hàm playGame của bạn nó sẽ trông cả hai như:

-- transformers approach :: (Num s) => StateT s IO() 
do x <- get 
    y <- lift $ someIOAction 
    put $ x + y 

-- mtl approach :: (Num s, MonadState s m, MonadIO m) => m() 
do x <- get 
    y <- liftIO $ someIOAction 
    put $ x + y 

Có nhiều sự khác biệt giữa các cách tiếp cận trở nên rõ ràng hơn khi bạn bắt đầu xếp chồng nhiều hơn một biến đơn lẻ, nhưng tôi nghĩ đó là một khởi đầu tốt cho bây giờ.

+0

Câu trả lời rất hay và đầy đủ. Cảm ơn. – Ralph

+1

'StateT IO (Chuỗi, Bool)' không chính xác - một loại không phù hợp. Đó là 'StateT s m a' với' s' kiểu trạng thái và 'm' một đơn vị, và' a' kiểu kết quả. –

+0

Ngoài ra, phương pháp 'mtl' và phương pháp' máy biến áp' thực sự không phải là những cách khác nhau để làm điều tương tự - 'MonadError' đạt được mục tiêu khác với' MonadTrans'. Và luật pháp không phải là luật 'Monad' trong ngụy trang - chúng rất giống nhau *, nhưng những thứ liên quan và ý nghĩa của chúng thì khác nhau. Ý tôi là, về cơ bản chúng là luật đơn vị và luật liên kết, nhưng đối với các hoạt động hoàn toàn khác nhau. –

8

State là một đơn nguyên và IO là một đơn nguyên. Những gì bạn đang cố gắng để viết từ đầu được gọi là một "biến áp monad", và thư viện chuẩn Haskell đã xác định những gì bạn cần.

Hãy xem biến trạng thái đơn vị StateT: nó có tham số là đơn vị bên trong bạn muốn quấn vào State.

Mỗi máy biến áp đơn lẻ thực hiện một loạt các kiểu chữ, ví dụ cho mỗi trường hợp, biến áp xử lý nó mỗi khi có thể (ví dụ: biến áp trạng thái chỉ có thể xử lý trực tiếp các chức năng liên quan đến trạng thái), hoặc nó truyền bá cuộc gọi để các đơn nguyên bên trong theo cách như vậy mà khi bạn có thể ngăn xếp tất cả các máy biến áp bạn muốn, và có một giao diện thống nhất để truy cập vào các tính năng của tất cả chúng. Đó là một loại chain of responsibility, nếu bạn muốn xem nó theo cách này.

Nếu bạn nhìn vào hackage hoặc thực hiện tìm kiếm nhanh trên tràn ngăn xếp hoặc google, bạn sẽ tìm thấy rất nhiều ví dụ về tập quán StateT.

chỉnh sửa: Một cách đọc thú vị khác là Monad Transformers Explained.

+1

Tôi thích cách khám phá bánh xe nhiều lần học Haskell ... Thật sự rất hay khi tìm ra giải pháp cho vấn đề và tìm ra, đó là một mẫu thiết kế chung. – fuz

+1

@FUZxxl: Có, nó thực sự là :) –

2

Được rồi, một số điều cần làm sáng tỏ ở đây:

  • Bạn không thể "trả về một đơn nguyên". Một đơn nguyên là loại loại, không phải là một loại giá trị (chính xác, một đơn nguyên là một hàm tạo kiểu có một phiên bản của lớp Monad). Tôi biết điều này nghe có vẻ phức tạp, nhưng nó có thể giúp bạn phân biệt sự khác biệt giữa mọi thứ và loại vật trong đầu bạn, điều đó quan trọng.
  • Lưu ý rằng bạn không thể làm bất cứ điều gì với State đó là không thể mà không có nó, vì vậy nếu bạn đang bối rối về cách sử dụng nó, sau đó không cảm thấy bạn cần! Thông thường, tôi chỉ viết kiểu hàm bình thường mà tôi muốn, và sau đó nếu tôi nhận thấy tôi có rất nhiều chức năng có hình dạng như Thing -> (Thing, a) Tôi sẽ đi "aha, cái này trông giống như State, có lẽ điều này có thể được đơn giản hóa thành State Thing a". Hiểu và làm việc với các chức năng đơn giản là bước quan trọng đầu tiên trên đường sử dụng State hoặc bạn bè của nó. Mặt khác,
  • IO là thứ duy nhất có thể thực hiện công việc của mình. Nhưng cái tên playGame không ngay lập tức nổi bật với tôi như tên của cái gì đó cần phải làm I/O.Cụ thể, nếu bạn chỉ chỉ số ngẫu nhiên cần (giả), bạn có thể thực hiện điều đó mà không cần IO. Như một người bình luận đã chỉ ra, MonadRandom là điều tuyệt vời để làm điều này đơn giản, nhưng một lần nữa bạn chỉ có thể sử dụng các hàm thuần túy lấy và trả về một StdGen từ System.Random. Bạn chỉ cần chắc chắn rằng bạn chủ đề hạt giống của bạn (các StdGen) một cách chính xác (làm điều này tự động là cơ bản lý do tại sao State được phát minh; bạn có thể tìm thấy bạn hiểu nó tốt hơn sau khi cố gắng để chương trình mà không có nó!)
  • Cuối cùng, bạn không hoàn toàn sử dụng getStdGen một cách chính xác. Đó là hành động IO, vì vậy bạn cần phải kết buộc kết quả của nó với <- trong khóa do trước khi sử dụng nó (về mặt kỹ thuật, bạn không cần, bạn có rất nhiều tùy chọn, nhưng gần như chắc chắn bạn muốn làm gì) . Một cái gì đó như thế này:

    do 
        seed <- getStdGen 
        results <- mapM (\game -> playGame game seed) [1..numberOfGames] 
    

    Đây playGame :: Integer -> StdGen -> IO (String, Bool). Tuy nhiên, lưu ý rằng bạn đang chuyển số giống nhau hạt giống ngẫu nhiên cho mỗi playGame, có thể hoặc không thể là thứ bạn muốn. Nếu nó không phải là, tốt, bạn có thể trả lại hạt giống từ mỗi playGame khi bạn đã thực hiện với nó, để vượt qua để tiếp theo, hoặc liên tục nhận hạt giống mới với newStdGen (mà bạn có thể làm từ bên trong playGame, nếu bạn quyết định giữ nó trong IO).

Dù sao, đây không phải là câu trả lời rất có cấu trúc, mà tôi xin lỗi, nhưng tôi hy vọng nó sẽ cho bạn điều gì đó để suy nghĩ.

+0

"* Một đơn nguyên là một loại * loại, * không phải là một loại giá trị. *" Nó sẽ không công bằng để nói rằng nó thực sự là một nhà xây dựng kiểu? – Ashe

+1

Có, một đơn nguyên là một loại nhà xây dựng kiểu, nhưng tôi không muốn có vẻ quá kỹ thuật - tôi chỉ muốn nhấn mạnh rằng "monads thuộc về thế giới của các loại". Tôi sẽ chỉnh sửa nó chính xác hơn. –

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