2012-06-02 36 views
9

Tôi đang cố viết một trò chơi nhỏ trong Haskell và có một số lượng trạng thái cần thiết để vượt qua. Tôi muốn thử che giấu tiểu bang bằng đơn vị Tiểu bangSử dụng trình đơn trạng thái để ẩn trạng thái rõ ràng

Bây giờ tôi đã gặp phải vấn đề: các chức năng đưa tiểu bang và tranh luận trở nên dễ dàng để viết để làm việc trong tiểu bang. Nhưng cũng có các hàm chỉ lấy trạng thái làm đối số (và trả về trạng thái đã sửa đổi, hoặc có thể là một cái gì đó khác).

Trong một phần của mã của tôi, tôi có dòng này:

let player = getCurrentPlayer state 

Tôi muốn nó để không phải đi tiểu bang, và thay vì viết

player <- getCurrentPlayerM 

hiện nay, việc thực hiện nó trông như thế này

getCurrentPlayer gameState = 
    (players gameState) ! (on_turn gameState) 

và dường như đủ đơn giản để làm cho nó hoạt động trong đơn nguyên trạng thái bằng cách viết nó như sau:

getCurrentPlayerM = do state <- get 
         return (players state ! on_turn state) 

Tuy nhiên, điều đó gây nên khiếu nại từ ghc! Không có trường hợp nào (MonadState GameState m0) phát sinh từ việc sử dụng `get ', nó nói. Tôi đã viết lại một chức năng rất giống nhau, ngoại trừ việc không nullary ở dạng đơn nguyên nhà nước của Bộ, vì vậy trên một linh cảm, tôi viết lại nó như thế này:

getCurrentPlayerM _ = do state <- get 
         return (players state ! on_turn state) 

Và chắc chắn đủ, nó hoạt động! Nhưng tất nhiên tôi phải gọi nó là getCurrentPlayerM(), và tôi cảm thấy một chút ngớ ngẩn khi làm điều đó. Đi qua trong một cuộc tranh luận là những gì tôi muốn tránh ngay từ đầu!

Một ngạc nhiên khác: nhìn vào loại của nó trong ghci tôi nhận được

getCurrentPlayerM :: MonadState GameState m => t -> m P.Player 

nhưng nếu tôi cố gắng thiết lập mà rõ ràng trong mã của tôi, tôi nhận được một lỗi: "Không loại biến lập luận trong ràng buộc MonadState GameState m "và một lời đề nghị của một phần mở rộng ngôn ngữ để cho phép nó. Tôi cho rằng đó là vì GameState của tôi là một loại và không phải là một kiểu chữ, nhưng tại sao nó được chấp nhận trong thực tế nhưng không phải khi tôi cố gắng rõ ràng về nó, tôi thấy bối rối hơn.

Vì vậy, để tổng hợp:

  1. Tại sao tôi không thể viết các hàm nullary trong đơn nguyên nhà nước?
  2. Tại sao tôi không thể khai báo loại chức năng workaround của tôi thực sự có?

Trả lời

14

Vấn đề là bạn không viết chữ ký kiểu cho các chức năng của mình và hạn chế monomorphism sẽ áp dụng.

Khi bạn viết:

getCurrentPlayerM = ... 

bạn đang viết một unary định nghĩa giá trị hạn chế cấp cao nhất mà không có một lời tuyên bố loại, vì vậy trình biên dịch Haskell sẽ cố gắng để suy ra một kiểu cho định nghĩa. Tuy nhiên, hạn chế monomorphism (nghĩa đen: hạn chế hình đơn) nói rằng tất cả các định nghĩa cấp cao nhất với các ràng buộc kiểu suy luận phải giải quyết cho các loại cụ thể, tức là chúng không phải là đa hình.


Để giải thích những gì tôi có nghĩa là, lấy ví dụ này đơn giản hơn:

pi = 3.14 

Ở đây, chúng ta định nghĩa pi mà không có một loại, vì vậy GHC suy luận kiểu Fractional a => a, tức là "bất kỳ loại a, miễn là nó có thể được coi như một phần nhỏ. " Tuy nhiên, loại này là có vấn đề, bởi vì nó có nghĩa là pi không phải là một hằng số, mặc dù nó trông giống như nó được. Tại sao? Bởi vì giá trị của pi sẽ được tính lại tùy thuộc vào loại chúng tôi muốn.

Nếu chúng tôi có (2::Double) + pi, pi sẽ là Double. Nếu chúng tôi có (3::Float) + pi, pi sẽ là Float. Mỗi lần sử dụng pi, do đó, nó phải được tính lại (vì chúng tôi không thể lưu trữ các phiên bản thay thế của pi cho tất cả các loại phân đoạn có thể, có thể không?). Điều này là tốt cho các chữ đơn giản 3.14, nhưng nếu chúng ta muốn nhiều thập phân của pi và sử dụng một thuật toán ưa thích tính nó? Chúng tôi sẽ không muốn nó được tính toán lại mỗi khi pi được sử dụng sau đó, phải không?

Đây là lý do tại sao Báo cáo Haskell tuyên bố rằng các định nghĩa ràng buộc loại đơn nhất cấp cao nhất phải có một loại đơn (monomorphic), để tránh vấn đề này. Trong trường hợp này, pi sẽ nhận được loại default của Double. Bạn có thể thay đổi mặc định kiểu dữ liệu nếu bạn muốn, bằng cách sử dụng default keyword:

default (Int, Float) 

pi = 3.14 -- pi will now be Float 

Trong trường hợp của bạn, tuy nhiên, bạn đang nhận được chữ ký suy ra:

getCurrentPlayerM :: MonadState GameState m => m P.Player 

Nó có nghĩa là: "Đối với bất kỳ nhà nước đơn lẻ lưu trữ GameState s, truy xuất trình phát. " Tuy nhiên, do hạn chế monomorphism áp dụng, Haskell buộc phải cố gắng để làm cho loại này không đa hình, bằng cách chọn một loại cụ thể cho m. Tuy nhiên, nó không thể tìm thấy một, bởi vì không có loại mặc định cho monads nhà nước như có cho con số, do đó, nó cho lên.

Bạn có thể muốn từ bỏ chức năng của bạn một loại chữ ký rõ ràng:

getCurrentPlayerM :: MonadState GameState m => m P.Player 

... nhưng bạn sẽ phải thêm phần mở rộng ngôn ngữ Haskell FlexibleContexts cho nó hoạt động, bằng cách thêm này ở phía trên cùng của bạn file:

{-# LANGUAGE FlexibleContexts #-} 

Hoặc, bạn có thể xác định một cách rõ ràng mà nhà nước đơn nguyên bạn muốn:

getCurrentPlayerM :: State GameState P.Player 

Bạn cũng có thể vô hiệu hóa giới hạn đơn cấu hình, bằng cách thêm phần mở rộng cho điều đó; Tuy nhiên, tốt hơn hết là nên thêm chữ ký kiểu.

{-# LANGUAGE NoMonomorphismRestriction #-} 

PS. Nếu bạn có một hàm mang theo tiểu bang của bạn như một tham số, bạn có thể sử dụng:

value <- gets getCurrentPlayer 

Bạn cũng nên xem xét sử dụng Lenses với State monads, cho phép bạn viết mã rất sạch sẽ cho đi qua trạng thái tiềm ẩn.

+1

Hiển thị rõ ràng đơn nguyên mà tôi muốn có vẻ là giải pháp đúng trong trường hợp của tôi. Có, tôi đã chạy vào vấn đề với các hồ sơ trong hồ sơ, và đọc một chút về Ống kính (trong số những thứ khác, khi tôi tìm kiếm câu trả lời ở đây, nó dường như được đề nghị rất nhiều!). Cảm ơn, giải thích tuyệt vời! –

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