2012-03-03 24 views
21

Tôi có câu hỏi về cách tốt nhất để thiết kế chương trình tôi đang làm việc trên Haskell. Tôi đang viết một mô phỏng vật lý, đó là điều mà tôi đã thực hiện một loạt các ngôn ngữ mệnh lệnh tiêu chuẩn, và thường là phương pháp chính trông giống như sau:Thiết kế chương trình trong Haskell: cách thực hiện mô phỏng mà không có khả năng biến đổi

while True: 
    simulationState = stepForward(simulationState) 
    render(simulationState) 

Và tôi tự hỏi làm thế nào để làm điều gì đó tương tự trong Haskell . Tôi có hàm step :: SimState -> SimState và hàm display :: SimState -> IO() sử dụng HOpenGL để vẽ trạng thái mô phỏng, nhưng tôi thua lỗ về cách thực hiện điều này trong "vòng lặp" của các loại, vì tất cả các giải pháp mà tôi có thể đưa ra có liên quan một số loại biến đổi. Tôi là một chút noob khi nói đến Haskell, vì vậy nó hoàn toàn có thể là tôi đang thiếu một quyết định thiết kế rất rõ ràng. Ngoài ra, nếu có một cách tốt hơn để kiến ​​trúc sư chương trình của tôi như một toàn thể, tôi rất vui khi nghe nó.

Cảm ơn trước!

Trả lời

20

Theo tôi, cách đúng đắn để suy nghĩ về vấn đề này không phải là vòng lặp, mà là danh sách hoặc cấu trúc phát trực tuyến vô hạn khác. Tôi đã cho a similar answer đến a similar question; ý tưởng cơ bản là, như C. A. McCann wrote, để sử dụng iterate stepForward initialState, trong đó iterate :: (a -> a) -> a -> [a] "trả về danh sách vô hạn các ứng dụng lặp lại của [stepForward] đến [initialState]".

Vấn đề với cách tiếp cận này là bạn gặp khó khăn trong việc xử lý một bước monadic và đặc biệt là chức năng hiển thị đơn sắc. Một cách tiếp cận sẽ chỉ là lấy đoạn mong muốn của danh sách trước (có thể với một hàm như takeWhile, có thể với đệ quy thủ công) và sau đó mapM_ render trên đó. Một cách tiếp cận tốt hơn sẽ là sử dụng cấu trúc luồng khác nhau, thực chất đơn thuần, trực tuyến. Bốn điều mà tôi có thể nghĩ đến là:

  • The iteratee package, ban đầu được thiết kế để phát trực tuyến IO. Tôi nghĩ ở đây, các bước của bạn sẽ là một nguồn (enumerator) và hiển thị của bạn sẽ là một bồn rửa (iteratee); sau đó bạn có thể sử dụng một đường ống (một enumeratee) để áp dụng các chức năng và/hoặc lọc ở giữa.
  • The enumerator package, dựa trên cùng một ý tưởng; người ta có thể sạch hơn người kia.
  • The newer pipes package, tự lập hóa đơn là "lặp lại đúng" —nó mới hơn, nhưng ngữ nghĩa, ít nhất là với tôi, rõ ràng hơn, cũng như tên (Producer, ConsumerPipe).
  • The List package, cụ thể là ListT biến áp đơn nguyên của nó. Biến áp đơn nguyên này được thiết kế để cho phép bạn tạo danh sách các giá trị đơn điệu với cấu trúc hữu ích hơn [m a]; ví dụ, làm việc với các danh sách đơn điệu vô hạn trở nên dễ quản lý hơn. Gói này cũng tổng hợp nhiều chức năng trên danh sách thành a new type class. Nó cung cấp một hàm iterateM hai lần; các first time trong tổng quát đáng kinh ngạc, và second time chuyên ListT. Sau đó, bạn có thể sử dụng các chức năng như takeWhileM để lọc.

Lợi thế lớn để sửa đổi lặp lại chương trình của bạn trong một số cấu trúc dữ liệu, thay vì sử dụng đệ quy, là chương trình của bạn sau đó có thể làm những việc hữu ích với luồng kiểm soát. Không có gì quá vĩ đại, tất nhiên, nhưng ví dụ, nó tách ra quyết định "làm thế nào để chấm dứt" từ quá trình "làm thế nào để tạo ra". Bây giờ, người dùng (ngay cả khi đó chỉ là bạn) có thể quyết định khi nào dừng lại: sau khi n bước? Sau khi tiểu bang thỏa mãn một vị ngữ nào đó? Không có lý do để bog xuống mã tạo của bạn với những quyết định này, vì nó là một mối quan tâm riêng biệt về mặt logic.

+1

Danh sách của bạn dường như bị thiếu [gói 'monad-loops'] (http://hackage.haskell.org/package/monad-loops), mà tôi nghĩ thực sự là minh chứng rõ ràng nhất về cách tiếp cận này. –

+0

Fantastic - Tôi đã tìm kiếm một lý do để tìm hiểu iteratees. Tôi sẽ xem xét gói ống. Cám ơn rất nhiều! –

+3

nó là quá mức cần thiết cho câu hỏi ban đầu, nhưng vì lợi ích của những người có thể đến sau khi tôi nghĩ rằng chúng ta nên đề cập đến [Lập trình phản ứng chức năng] (http://stackoverflow.com/questions/3154701/help-understanding-arrows-in- haskell) nói riêng [Yampa/Animas] (http://www.haskell.org/haskellwiki/Yampa). –

11

cách tiếp cận của bạn là ok, bạn chỉ cần phải nhớ rằng vòng được thể hiện dưới dạng đệ quy trong Haskell:

simulation state = do 
    let newState = stepForward state 
    render newState 
    simulation newState 

(. Nhưng bạn definietly cần một tiêu chí như thế nào để kết thúc vòng lặp)

+0

Chỉ để xác nhận, điều này sẽ không ngăn xếp tràn vì nó là đệ quy đuôi? –

+3

Nó không phải là đệ quy đuôi và cũng không nên chồng tràn :) Hãy thử hoặc thử một trong các giải pháp khác để sắp xếp một danh sách các trạng thái được hiển thị. – Ingo

+7

@haldean Nó sẽ không tràn ngăn xếp, mặc dù vì những lý do khác nhau. Đuôi đệ quy không phải là hữu ích hoặc quan trọng trong Haskell như trong các ngôn ngữ khác, do lười biếng. – delnan

20

Vâng, nếu vẽ các trạng thái kế tiếp là tất cả bạn muốn làm, điều đó khá đơn giản. Trước tiên, hãy thực hiện chức năng step và trạng thái ban đầu của bạn và sử dụng the iterate function. iterate step initialState sau đó là danh sách (vô hạn) của mỗi trạng thái mô phỏng. Sau đó bạn có thể ánh xạ display qua đó để có được hành động IO để vẽ mỗi tiểu bang, vì vậy cùng bạn muốn có một cái gì đó như thế này:

allStates :: [SimState] 
allStates = iterate step initialState 

displayedStates :: [IO()] 
displayedStates = fmap display allStates 

Cách đơn giản nhất để chạy nó sẽ được sử dụng sau đó the intersperse function để đặt một "chậm trễ "hành động giữa mỗi hành động hiển thị, sau đó sử dụng the sequence_ function để chạy toàn bộ điều:

main :: IO() 
main = sequence_ $ intersperse (delay 20) displayedStates 

Tất nhiên đó có nghĩa là bạn phải buộc chấm dứt việc áp dụng và ngăn cản bất kỳ loại tương tác, vì vậy nó không thực sự là một cách tốt để làm nó nói chung.

Một cách tiếp cận hợp lý hơn sẽ là xen kẽ những thứ như "xem liệu ứng dụng có nên thoát" ở từng bước hay không. Bạn có thể làm điều đó với đệ quy rõ ràng:

runLoop :: SimState -> IO() 
runLoop st = do display st 
       isDone <- checkInput 
       if isDone then return() 
          else delay 20 >> runLoop (step st) 

Cách tiếp cận ưa thích của tôi là viết các bước không đệ quy thay thế và sau đó sử dụng bộ kết hợp vòng lặp trừu tượng hơn. Đáng tiếc là có không hỗ trợ thực sự tốt để làm nó theo cách đó trong các thư viện chuẩn, nhưng nó sẽ giống như thế này:

runStep :: SimState -> IO SimState 
runStep st = do display st 
       delay 20 
       return (step st) 

runLoop :: SimState -> IO() 
runLoop initialState = iterUntilM_ checkInput runStep initialState 

Thực hiện iterUntilM_ chức năng là trái như một bài tập cho người đọc, heh.

+0

Giải pháp lặp/fmap là tuyệt vời, nhưng tôi sẽ đi với phương pháp recusion. Cám ơn rất nhiều! –

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