2012-12-17 35 views
14

Tôi đang viết một chương trình chạy dưới dạng daemon. Để tạo daemon, người sử dụng cung cấp một tập hợp các triển khai cho mỗi lớp học cần thiết (một trong số đó là một cơ sở dữ liệu) Tất cả những lớp học có chức năng có loại chữ ký của các hình thức StateT s IO a, nhưng s là khác nhau cho mỗi lớp.Kết hợp nhiều tiểu bang trong StateT

Giả sử mỗi người trong số các lớp học sau mô hình này:

import Control.Monad (liftM) 
import Control.Monad.State (StateT(..), get) 

class Hammer h where 
    driveNail :: StateT h IO() 

data ClawHammer = MkClawHammer Int -- the real implementation is more complex 

instance Hammer ClawHammer where 
    driveNail = return() -- the real implementation is more complex 

-- Plus additional classes for wrenches, screwdrivers, etc. 

Bây giờ tôi có thể xác định một kỷ lục mà đại diện cho việc thực hiện lựa chọn bởi người dùng cho mỗi "khe".

data MultiTool h = MultiTool { 
    hammer :: h 
    -- Plus additional fields for wrenches, screwdrivers, etc. 
    } 

Và daemon làm hầu hết công việc của mình trong đơn nguyên StateT (MultiTool h ...) IO() .

Bây giờ, vì multitool chứa một cái búa, tôi có thể sử dụng nó trong mọi tình huống nơi cần búa. Nói cách khác, các MultiTool loại có thể thực hiện bất kỳ của các lớp mà nó chứa, nếu tôi viết mã như thế này:

stateMap :: Monad m => (s -> t) -> (t -> s) -> StateT s m a -> StateT t m a 
stateMap f g (StateT h) = StateT $ liftM (fmap f) . h . g 

withHammer :: StateT h IO() -> StateT (MultiTool h) IO() 
withHammer runProgram = do 
    t <- get 
    stateMap (\h -> t {hammer=h}) hammer runProgram 

instance Hammer h => Hammer (MultiTool h) where 
    driveNail = withHammer driveNail 

Nhưng việc triển khai của withHammer, withWrench, withScrewdriver vv về cơ bản giống hệt nhau. Sẽ thật tuyệt nếu có thể viết một cái gì đó như thế này ...

--withMember accessor runProgram = do 
-- u <- get 
-- stateMap (\h -> u {accessor=h}) accessor runProgram 

-- instance Hammer h => Hammer (MultiTool h) where 
-- driveNail = withMember hammer driveNail 

Nhưng tất nhiên điều đó sẽ không biên dịch.

Tôi nghi ngờ giải pháp của mình quá hướng đối tượng. Có cách nào tốt hơn không? Biến thế Monad, có thể? Cảm ơn bạn trước vì bất kỳ đề xuất nào.

+0

Ngẫu nhiên, tôi đã chỉnh sửa nhanh mã của bạn vì đơn giản hóa của bạn bỏ qua việc thực hiện 'ClawHammer' bạn đã tạo ra thứ gì đó có lẽ không phải là ý của bạn. –

Trả lời

24

Nếu bạn muốn đi với một trạng thái toàn cầu lớn như trong trường hợp của bạn, thì những gì bạn muốn sử dụng là ống kính, theo đề xuất của Ben. Tôi cũng khuyên bạn nên sử dụng thư viện của Edward Kmett. Tuy nhiên, có một cách khác, có lẽ đẹp hơn.

Máy chủ có thuộc tính mà chương trình chạy liên tục và thực hiện thao tác tương tự trên vùng trạng thái.Sự cố bắt đầu khi bạn muốn mô đun hóa máy chủ của mình, trong trường hợp đó bạn muốn nhiều hơn chỉ là một số trạng thái toàn cầu. Bạn muốn các mô-đun có trạng thái riêng của họ.

Hãy nghĩ về một mô-đun như một cái gì đó mà biến đổi một Yêu cầu đến một phản ứng :

Module :: (Request -> m Response) -> Module m 

Bây giờ nếu nó có một số nhà nước, sau đó tình trạng này trở nên đáng chú ý trong đó các mô-đun có thể cung cấp một khác nhau trả lời lần sau. Có một số cách để làm điều này, ví dụ như sau:

Module :: s -> ((Request, s) -> m (Response s)) -> Module m 

Nhưng một cách rất đẹp và tương đương để diễn tả điều này được các nhà xây dựng sau (chúng tôi sẽ xây dựng một loại xung quanh nó sớm):

Module :: (Request -> m (Response, Module m)) -> Module m 

Mô-đun này ánh xạ yêu cầu phản hồi, nhưng trên đường đi cũng trả về phiên bản mới của chính nó. Hãy tiếp tục và đưa ra yêu cầu và phản hồi đa hình:

Module :: (a -> m (b, Module m a b)) -> Module m a b 

Bây giờ nếu loại đầu ra của mô-đun khớp với loại đầu vào của mô-đun khác, bạn có thể tạo chúng như chức năng thông thường. Thành phần này là kết hợp và có bản sắc đa hình. Điều này nghe có vẻ giống như một thể loại, và trên thực tế nó là! Nó là một thể loại, một functor ứng dụng và một mũi tên.

newtype Module m a b = 
    Module (a -> m (b, Module m a b)) 

instance (Monad m) => Applicative (Module m a) 
instance (Monad m) => Arrow (Module m) 
instance (Monad m) => Category (Module m) 
instance (Monad m) => Functor (Module m a) 

Hiện tại, chúng tôi có thể soạn hai mô-đun có trạng thái địa phương riêng mà không hề biết! Nhưng điều đó không đủ. Chúng tôi muốn nhiều hơn nữa. Làm thế nào về các mô-đun có thể được chuyển đổi giữa? Chúng ta hãy mở rộng hệ thống mô-đun nhỏ của chúng tôi như vậy mà module thực sự có thể chọn không để đưa ra một câu trả lời:

newtype Module m a b = 
    Module (a -> m (Maybe b, Module m a b)) 

Điều này cho phép một hình thức sáng tác mà là trực giao với (.): Bây giờ loại của chúng tôi cũng là một gia đình của Alternative functors:

instance (Monad m) => Alternative (Module m a) 

Bây giờ, một mô-đun có thể chọn trả lời yêu cầu và nếu không, mô-đun tiếp theo sẽ được thử. Đơn giản. Bạn vừa mới phát minh lại danh mục dây. =)

Tất nhiên bạn không cần phải tái tạo lại điều này. Thư viện Netwire thực hiện mẫu thiết kế này và đi kèm với một thư viện lớn được xác định trước "mô-đun" (được gọi là dây). Xem mô-đun Control.Wire để biết hướng dẫn.

+5

Câu trả lời vô cùng xuất sắc! – AndrewC

6

Điều này nghe rất giống với ứng dụng của ống kính.

Ống kính là đặc điểm kỹ thuật của trường phụ của một số dữ liệu. Ý tưởng là bạn có một số giá trị toolLens và chức năng viewset để view toolLens :: MultiTool h -> h tìm nạp công cụ và set toolLens :: MultiTool h -> h -> MultiTool h thay thế bằng một giá trị mới. Sau đó, bạn có thể dễ dàng xác định withMember của mình làm chức năng chỉ chấp nhận ống kính.

Công nghệ ống kính đã cải tiến rất nhiều trong thời gian gần đây và giờ đây chúng có khả năng vô cùng cao. Thư viện mạnh nhất xung quanh tại thời điểm viết thư là thư viện lens của Edward Kmett, hơi khó nuốt nhưng khá đơn giản khi bạn tìm thấy các tính năng bạn muốn. Bạn cũng có thể tìm kiếm thêm câu hỏi về các ống kính ở đây trên SO, ví dụ: Functional lenses liên kết với lenses, fclabels, data-accessor - which library for structure access and mutation is better hoặc thẻ lenses.

14

Dưới đây là ví dụ cụ thể về cách sử dụng lens như mọi người khác đang nói đến. Trong ví dụ mã sau, Type1 là trạng thái cục bộ (nghĩa là búa của bạn) và Type2 là trạng thái toàn cục (tức là công cụ đa nhiệm của bạn). lens cung cấp các chức năng zoom cho phép bạn chạy một tính toán nhà nước địa phương mà phóng to trên bất kỳ lĩnh vực xác định bởi một ống kính:

import Control.Lens 
import Control.Monad.Trans.Class (lift) 
import Control.Monad.Trans.State 

data Type1 = Type1 { 
    _field1 :: Int , 
    _field2 :: Double} 

field1 :: SimpleLens Type1 Int 
field1 = lens _field1 (\x a -> x { _field1 = a}) 

field2 :: SimpleLens Type1 Double 
field2 = lens _field2 (\x a -> x { _field2 = a}) 

data Type2 = Type2 { 
    _type1 :: Type1 , 
    _field3 :: String} 

type1 :: SimpleLens Type2 Type1 
type1 = lens _type1 (\x a -> x { _type1 = a}) 

field3 :: SimpleLens Type2 String 
field3 = lens _field3 (\x a -> x { _field3 = a}) 

localCode :: StateT Type1 IO() 
localCode = do 
    field1 += 3 
    field2 .= 5.0 
    lift $ putStrLn "Done!" 

globalCode :: StateT Type2 IO() 
globalCode = do 
    f1 <- zoom type1 $ do 
     localCode 
     use field1 
    field3 %= (++ show f1) 
    f3 <- use field3 
    lift $ putStrLn f3 

main = runStateT globalCode (Type2 (Type1 9 4.0) "Hello: ") 

zoom không được giới hạn trước mắt tiểu lĩnh vực của một loại.Vì các ống kính có thể ghép được, bạn có thể phóng to đến mức bạn muốn trong một thao tác đơn giản bằng cách thực hiện một cái gì đó như:

zoom (field1a . field2c . field3b . field4j) $ do ... 
+0

Nhược điểm cuối cùng của phương pháp này là 'Type1' được lồng trực tiếp bên trong' Type2' và kiến ​​thức đầy đủ về loại đó là bắt buộc. Điều đó làm cho IMHO bị rò rỉ trừu tượng. –

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