Sự khác biệt giữa State, ST, IORef và MVar


91

Tôi đang làm việc thông qua việc Viết cho chính bạn một Đề án trong 48 giờ (tôi lên đến khoảng 85 giờ) và tôi đã bắt đầu phần Thêm biến và Bài tập . Có một bước nhảy lớn về mặt khái niệm trong chương này, và tôi ước nó được thực hiện theo hai bước với một cấu trúc lại tốt ở giữa thay vì chuyển thẳng đến giải pháp cuối cùng. Dù sao…

Tôi đã nhận được mất với một số lớp khác nhau mà dường như để phục vụ cùng một mục đích: State, ST, IORef, và MVar. Ba câu đầu tiên được đề cập trong văn bản, trong khi câu cuối cùng dường như là câu trả lời ưa thích cho rất nhiều câu hỏi của StackOverflow về ba câu đầu tiên. Tất cả chúng dường như mang một trạng thái giữa các lần gọi liên tiếp.

Mỗi cái này là gì và chúng khác nhau như thế nào?


Đặc biệt những câu này không có ý nghĩa:

Thay vào đó, chúng tôi sử dụng một tính năng gọi là tiểu trình trạng thái , cho phép Haskell quản lý trạng thái tổng hợp cho chúng tôi. Điều này cho phép chúng tôi xử lý các biến có thể thay đổi như chúng tôi làm trong bất kỳ ngôn ngữ lập trình nào khác, sử dụng các hàm để lấy hoặc đặt biến.

Mô-đun IORef cho phép bạn sử dụng các biến trạng thái trong đơn nguyên IO .

Tất cả điều này làm cho dòng type ENV = IORef [(String, IORef LispVal)]khó hiểu - tại sao lại là thứ hai IORef? Điều gì sẽ phá vỡ nếu tôi viết type ENV = State [(String, LispVal)]thay thế?

Câu trả lời:


119

State Monad: một mô hình trạng thái có thể thay đổi

Đơn nguyên trạng thái là một môi trường hoàn toàn có chức năng cho các chương trình có trạng thái, với một API đơn giản:

  • được
  • đặt

Tài liệu trong gói mtl .

State monad thường được sử dụng khi cần trạng thái trong một luồng điều khiển duy nhất. Nó không thực sự sử dụng trạng thái có thể thay đổi trong quá trình triển khai. Thay vào đó, chương trình được tham số hóa bởi giá trị trạng thái (nghĩa là trạng thái là một tham số bổ sung cho tất cả các phép tính). Trạng thái chỉ dường như bị đột biến trong một luồng duy nhất (và không thể được chia sẻ giữa các luồng).

Đơn nguyên ST và STRefs

Đơn nguyên ST là anh em họ bị hạn chế của đơn nguyên IO.

Nó cho phép trạng thái có thể thay đổi tùy ý , được thực hiện như bộ nhớ có thể thay đổi thực tế trên máy. API được tạo an toàn trong các chương trình không có tác dụng phụ, vì tham số loại xếp hạng 2 ngăn các giá trị phụ thuộc vào trạng thái có thể thay đổi thoát khỏi phạm vi cục bộ.

Do đó, nó cho phép kiểm soát khả năng thay đổi trong các chương trình thuần túy khác.

Thường được sử dụng cho các mảng có thể thay đổi và các cấu trúc dữ liệu khác bị đột biến, sau đó được đóng băng. Nó cũng rất hiệu quả, vì trạng thái có thể thay đổi được "tăng tốc phần cứng".

API chính:

  • Control.Monad.ST
  • runST - bắt đầu tính toán hiệu ứng bộ nhớ mới.
  • STRefs : con trỏ đến các ô có thể thay đổi (cục bộ).
  • Mảng dựa trên ST (chẳng hạn như vectơ) cũng rất phổ biến.

Hãy coi nó như là người anh em ít nguy hiểm hơn của đơn nguyên IO. Hoặc IO, nơi bạn chỉ có thể đọc và ghi vào bộ nhớ.

IORef: STRef trong IO

Đây là các STRef (xem ở trên) trong đơn nguyên IO. Họ không có các đảm bảo an toàn giống như STRefs về địa phương.

MVars: IORefs có khóa

Giống như STRefs hoặc IORefs, nhưng có gắn một khóa để truy cập đồng thời an toàn từ nhiều luồng. IORefs và STRefs chỉ an toàn trong cài đặt đa luồng khi sử dụng atomicModifyIORef(hoạt động nguyên tử so sánh và hoán đổi). MVars là một cơ chế chung hơn để chia sẻ trạng thái có thể thay đổi một cách an toàn.

Nói chung, trong Haskell, sử dụng MVars hoặc TVars (các ô có thể thay đổi dựa trên STM), trên STRef hoặc IORef.


3
Chữ M trong MVars và T trong TVars là gì? Tôi đang đoán "Có thể thay đổi", "Giao dịch". Điều thú vị là ST có nghĩa là State Thread.
CMCDragonkai

10
Tại sao bạn nói rằng MVarnên được ưu tiên hơn STRef? STRefđảm bảo rằng chỉ một luồng có thể thay đổi nó (và không phải các loại IO khác có thể xảy ra) - chắc chắn điều đó tốt hơn nếu tôi không cần truy cập đồng thời vào trạng thái có thể thay đổi?
Benjamin Hodgson

@CMCDragonkai Tôi luôn cho rằng M là viết tắt của mutex, nhưng tôi không thể tìm thấy nó ở bất kỳ đâu.
Andrew Thaddeus Martin

37

Ok, tôi sẽ bắt đầu với IORef. IORefcung cấp một giá trị có thể thay đổi trong đơn nguyên IO. Nó chỉ là một tham chiếu đến một số dữ liệu và giống như bất kỳ tham chiếu nào, có những hàm cho phép bạn thay đổi dữ liệu mà nó đề cập đến. Trong Haskell, tất cả các chức năng đó đều hoạt động IO. Bạn có thể coi nó giống như một cơ sở dữ liệu, tệp hoặc kho lưu trữ dữ liệu bên ngoài khác - bạn có thể lấy và thiết lập dữ liệu trong đó, nhưng làm như vậy đòi hỏi phải thông qua IO. Lý do IO cần thiết là vì Haskell là nguyên chất ; trình biên dịch cần một cách để biết dữ liệu nào mà tham chiếu trỏ đến tại bất kỳ thời điểm nào (đọc bài đăng trên blog "Bạn có thể đã phát minh ra monads" của sigfpe).

MVarVề cơ bản s là những thứ giống như IORef, ngoại trừ hai điểm khác biệt rất quan trọng. MVarlà một nguyên thủy đồng thời, vì vậy nó được thiết kế để truy cập từ nhiều luồng. Sự khác biệt thứ hai là an MVarlà một hộp có thể đầy hoặc rỗng. Vì vậy, trong trường hợp một IORef Intluôn luôn có một Int(hoặc là dưới cùng), một MVar Intcó thể có một Inthoặc nó có thể trống. Nếu một luồng cố gắng đọc một giá trị từ một giá trị trống MVar, nó sẽ chặn cho đến khi MVarđược lấp đầy (bởi một luồng khác). Về cơ bản một MVar atương đương với một IORef (Maybe a)với ngữ nghĩa bổ sung rất hữu ích cho đồng thời.

Statelà một đơn nguyên cung cấp trạng thái có thể thay đổi, không nhất thiết phải có IO. Trên thực tế, nó đặc biệt hữu ích cho các phép tính thuần túy. Nếu bạn có một thuật toán sử dụng trạng thái nhưng không IO, một Stateđơn nguyên thường là một giải pháp tốt.

Ngoài ra còn có một phiên bản biến áp đơn nguyên của State , StateT. Điều này thường được sử dụng để giữ dữ liệu cấu hình chương trình hoặc các loại trạng thái "game-world-state" trong các ứng dụng.

STlà một cái gì đó hơi khác. Cấu trúc dữ liệu chính trong STSTRef, giống như một IORefnhưng với một đơn nguyên khác. Đơn STnguyên sử dụng thủ thuật hệ thống kiểu ("chuỗi trạng thái" mà tài liệu đề cập) để đảm bảo rằng dữ liệu có thể thay đổi không thể thoát khỏi đơn nguyên; nghĩa là, khi bạn chạy tính toán ST, bạn sẽ nhận được một kết quả thuần túy. Lý do ST thú vị là nó là một đơn nguyên nguyên thủy giống như IO, cho phép các tính toán thực hiện các thao tác cấp thấp trên bytearrays và con trỏ. Điều này có nghĩa là nó STcó thể cung cấp một giao diện thuần túy trong khi sử dụng các hoạt động cấp thấp trên dữ liệu có thể thay đổi, nghĩa là nó rất nhanh. Từ quan điểm của chương trình, nó giống như thể STtính toán chạy trong một luồng riêng biệt với lưu trữ cục bộ luồng.


17

Những người khác đã làm những điều cốt lõi, nhưng để trả lời câu hỏi trực tiếp:

Tất cả điều này làm cho kiểu dòng trở nên ENV = IORef [(String, IORef LispVal)] khó hiểu. Tại sao lại là IORef thứ hai? Điều gì sẽ phá vỡ nếu tôi làm type ENV = State [(String, LispVal)]thay thế?

Lisp là một ngôn ngữ chức năng với trạng thái có thể thay đổi và phạm vi từ vựng. Hãy tưởng tượng bạn đã đóng trên một biến có thể thay đổi. Bây giờ bạn đã có tham chiếu đến biến này quanh quẩn bên trong một số hàm khác - giả sử (trong mã giả kiểu haskell) (printIt, setIt) = let x = 5 in (\ () -> print x, \y -> set x y). Bây giờ bạn có hai hàm - một in x và một đặt giá trị của nó. Khi bạn đánh giá printIt, bạn muốn tra cứu tên của x trong môi trường ban đầu printItđã được xác định, nhưng bạn muốn tra cứu giá trị mà tên đó được liên kết trong môi trường printItđược gọi (sau đó setItcó thể được gọi bất kỳ số lần nào ).

Có nhiều cách phù hợp với hai IORef để thực hiện điều này, nhưng bạn chắc chắn cần nhiều hơn loại thứ hai mà bạn đã đề xuất, điều này không cho phép bạn thay đổi các giá trị mà tên được liên kết theo kiểu phạm vi từ vựng. Google "vấn đề funargs" cho rất nhiều điều thú vị thời tiền sử.

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.