9

Tôi đang cố gắng trừu tượng hóa mô hình áp dụng một ngữ nghĩa nhất định cho một đơn nguyên miễn phí trên một số hàm. Ví dụ đang chạy mà tôi đang sử dụng để thúc đẩy việc này là áp dụng các bản cập nhật cho một thực thể trong trò chơi. Vì vậy, tôi nhập một vài thư viện và xác định một vài loại ví dụ và một lớp thực thể cho các mục đích của ví dụ này (Tôi đang sử dụng việc thực hiện đơn nguyên miễn phí trong kiểm soát đơn nguyên-miễn phí):Áp dụng ngữ nghĩa cho các Monads miễn phí

{-# LANGUAGE DeriveFunctor #-} 
{-# LANGUAGE TypeFamilies #-} 

import Control.Monad.Free 
import Control.Monad.Identity 
import Control.Monad.Writer 

-- Things which can happen to an entity 
data Order = Order deriving Show 
data Damage = Damage deriving Show 

class Entity a where 
    evolve :: Double -> a -> a 
    order :: Order -> a -> a 
    damage :: Damage -> a -> a 

-- Make a trivial entity for testing purposes 
data Example = Example deriving Show 
instance Entity Example where 
    evolve _ a = a 
    order _ a = a 
    damage _ a = a 

-- A type to hold all the possible update types 
data EntityUpdate = 
     UpdateTime Double 
    | UpdateOrder Order 
    | UpdateDamage Damage 
    deriving (Show) 

-- Wrap UpdateMessage to create a Functor for constructing the free monad 
data UpdateFunctor cont = 
    UpdateFunctor {updateMessage :: EntityUpdate, continue :: cont} deriving (Show, Functor) 

-- Type synonym for the free monad 
type Update = Free UpdateEntity 

bây giờ tôi nhấc một số cơ bản cập nhật vào đơn nguyên:

liftF = wrap . fmap Pure 

updateTime :: Double -> Update() 
updateTime t = liftUpdate $ UpdateTime t 

updateOrder :: Order -> Update() 
updateOrder o = liftUpdate $ UpdateOrder o 

updateDamage :: Damage -> Update() 
updateDamage d = liftUpdate $ UpdateDamage d 

test :: Update() 
test = do 
    updateTime 8.0 
    updateOrder Order 
    updateDamage Damage 
    updateTime 4.0 
    updateDamage Damage 
    updateTime 6.0 
    updateOrder Order 
    updateTime 8.0 

Bây giờ chúng ta có đơn nguyên miễn phí, chúng ta cần phải cung cấp khả năng triển khai khác nhau, hoặc giải thích ngữ nghĩa, thẩm đơn nguyên như test trên. Các mô hình tốt nhất mà tôi có thể đưa ra cho việc này được đưa ra bởi các chức năng sau:

interpret :: (Monad m, Functor f, fm ~ Free f c) => (f fm -> fm) -> (f fm -> a -> m a) -> fm -> a -> m a 
interpret _ _ (Pure _ ) entity = return entity 
interpret c f (Impure u) entity = f u entity >>= interpret c f (c u) 

Sau đó, với một số chức năng ngữ nghĩa cơ bản chúng ta có thể cung cấp cho các giải thể hai sau, người ta như một đánh giá cơ bản và một như một đơn nguyên nhà văn preforming logging:

update (UpdateTime t) = evolve t 
update (UpdateOrder o) = order o 
update (UpdateDamage d) = damage d 

eval :: Entity a => Update() -> a -> a 
eval updates entity = runIdentity $ interpret continue update' updates entity where 
    update' u entity = return $ update (updateMessage u) entity 

logMessage (UpdateTime t) = "Simulating time for " ++ show t ++ " seconds.\n" 
logMessage (UpdateOrder o) = "Giving an order.\n" 
logMessage (UpdateDamage d) = "Applying damage.\n" 

evalLog :: Entity a => Update() -> a -> Writer String a 
evalLog = interpret continue $ \u entity -> do 
    let m = updateMessage u 
    tell $ logMessage m 
    return $ update m entity 

kiểm tra này trong GHCI:

> eval test Example 
Example 
> putStr . execWriter $ evalLog test Example 
Simulating time for 8.0 seconds. 
Giving an order. 
Applying damage. 
Simulating time for 4.0 seconds. 
Applying damage. 
Simulating time for 6.0 seconds. 
Giving an order. 
Simulating time for 8.0 seconds. 

này tất cả hoạt động tốt, nhưng nó mang lại cho tôi một cảm giác hơi khó chịu mà nó có thể là mo nói chung, hoặc có thể được tổ chức tốt hơn. Có để cung cấp một chức năng để cung cấp sự tiếp tục không rõ ràng lúc đầu và tôi không chắc chắn đó là cách tiếp cận tốt nhất. Tôi đã thực hiện một số nỗ lực để xác định lại interpret về các chức năng trong mô-đun Control.Monad.Free, chẳng hạn như foldFreeinduce. Nhưng tất cả chúng dường như không hoàn toàn hoạt động.

Tôi có đúng đường với điều này hoặc đang đánh giá sai không? Hầu hết các bài viết về monads miễn phí tôi đã tìm thấy tập trung vào hiệu quả của họ hoặc cách khác nhau để thực hiện chúng, chứ không phải trên các mẫu để thực sự sử dụng chúng như thế này. Nó cũng có vẻ mong muốn đóng gói này trong một số loại Semantic lớp, vì vậy tôi chỉ đơn giản có thể làm cho các trường hợp đơn nguyên khác nhau từ đơn nguyên miễn phí của tôi bằng cách gói functor trong một newtype và làm cho nó một thể hiện của lớp này. Tôi không thể hoàn toàn làm việc ra làm thế nào để làm điều này tuy nhiên.

CẬP NHẬT -

Tôi ước tôi có thể chấp nhận cả hai câu trả lời vì cả hai đều cực kỳ thông tin và được viết một cách chu đáo. Trong phần kết thúc, mặc dù chỉnh sửa để câu trả lời được chấp nhận bao gồm các chức năng sau khi tôi đã:

interpret :: (Functor m, Monad m) => (forall x. f x -> m x) -> Free f a -> m a 
interpret evalF = retract . hoistFree evalF 

(retracthoistFree là trong gói miễn phí Edward Kemmet trong Control.Monad.Free).

Tất cả ba trong số pipes, operationalsacundim's free-operational package rất phù hợp và trông giống như chúng sẽ rất hữu ích cho tôi trong tương lai. Cảm ơn tất cả.

Trả lời

3

Tôi không hiểu rõ ví dụ của bạn, nhưng tôi nghĩ bạn đang cơ bản xây dựng lại gói operational tại đây. Loại EntityUpdate của bạn rất giống với bộ hướng dẫn theo nghĩa là operationalUpdateFunctor của bạn giống như trình nhắn tin miễn phí trong tập lệnh — đó chính xác là công trình có liên quan operational và các bản miễn phí. (Xem "Is operational really isomorphic to a free monad?"this Reddit discussion).

Nhưng dù sao, gói operational có chức năng mà bạn muốn, interpretWithMonad:

interpretWithMonad :: forall instr m b. 
         Monad m => 
         (forall a. instr a -> m a) 
        -> Program instr b 
        -> m b 

này cho phép bạn để cung cấp một chức năng mà giải thích mỗi người trong số các hướng dẫn trong chương trình của bạn (mỗi giá trị EntityUpdate) như là một hành động monadic và chăm sóc phần còn lại.

Nếu tôi có thể được phép một chút của tự quảng bá, tôi chỉ vừa mới viết my own version of operational using free monads, vì tôi muốn có một phiên bản Applicative của operational 's Program loại. Kể từ khi ví dụ của bạn đánh tôi như là hoàn toàn áp dụng, tôi đã đi qua việc thực hiện bằng văn bản của bạn evalLog về thư viện của tôi, và tôi cũng có thể dán nó ở đây. (Tôi không thể hiểu được hàm eval của bạn.) Ở đây đi:

{-# LANGUAGE GADTs, ScopedTypeVariables, RankNTypes #-} 

import Control.Applicative 
import Control.Applicative.Operational 
import Control.Monad.Writer 

data Order = Order deriving Show 
data Damage = Damage deriving Show 

-- UpdateI is short for "UpdateInstruction" 
data UpdateI a where 
    UpdateTime :: Double -> UpdateI() 
    UpdateOrder :: Order -> UpdateI() 
    UpdateDamage :: Damage -> UpdateI() 

type Update = ProgramA UpdateI 

updateTime :: Double -> Update() 
updateTime = singleton . UpdateTime 

updateOrder :: Order -> Update() 
updateOrder = singleton . UpdateOrder 

updateDamage :: Damage -> Update() 
updateDamage = singleton . UpdateDamage 

test :: Update() 
test = updateTime 8.0 
    *> updateOrder Order 
    *> updateDamage Damage 
    *> updateTime 4.0 
    *> updateDamage Damage 
    *> updateTime 6.0 
    *> updateOrder Order 
    *> updateTime 8.0 

evalLog :: forall a. Update a -> Writer String a 
evalLog = interpretA evalI 
    where evalI :: forall x. UpdateI x -> Writer String x 
      evalI (UpdateTime t) = 
       tell $ "Simulating time for " ++ show t ++ " seconds.\n" 
      evalI (UpdateOrder Order) = tell $ "Giving an order.\n" 
      evalI (UpdateDamage Damage) = tell $ "Applying damage.\n" 

Output:

*Main> putStr $ execWriter (evalLog test) 
Simulating time for 8.0 seconds. 
Giving an order. 
Applying damage. 
Simulating time for 4.0 seconds. 
Applying damage. 
Simulating time for 6.0 seconds. 
Giving an order. 
Simulating time for 8.0 seconds. 

Bí quyết ở đây là giống như trong interpretWithMonad chức năng từ gói ban đầu, nhưng thích nghi với applicatives:

interpretA :: forall instr f a. Applicative f => 
       (forall x. instr x -> f x) 
      -> ProgramA instr a -> f a 

Nếu bạn thực sự cần một giải thích đơn thuần, nó chỉ là một mater nhập Control.Monad.Operational (hoặc là bản gốc hoặc của tôi) thay vì Control.Applicative.Operational và sử dụng Program thay vì ProgramA. ProgramA tuy nhiên mang đến cho bạn sức mạnh lớn hơn để kiểm tra chương trình tĩnh:

-- Sum the total time requested by updateTime instructions in an 
-- applicative UpdateI program. You can't do this with monads. 
sumTime :: ProgramA UpdateI() -> Double 
sumTime = sumTime' . viewA 
    where sumTime' :: forall x. ProgramViewA UpdateI x -> Double 
      sumTime' (UpdateTime t :<**> k) = t + sumTime' k 
      sumTime' (_ :<**> k) = sumTime' k 
      sumTime' (Pure _) = 0 

Ví dụ sử dụng sumTime:

*Main> sumTime test 
26.0 

EDIT: Nhìn lại, tôi nên đã cung cấp câu trả lời ngắn này. Điều này giả sử bạn đang sử dụng Control.Monad.Free từ gói của Edward Kmett:

interpret :: (Functor m, Monad m) => 
      (forall x. f x -> m x) 
      -> Free f a -> m a 
interpret evalF = retract . hoistFree evalF 
+0

Điều này thực sự hấp dẫn! Thời gian để tôi có một cái nhìn rất khó khăn về những phiên bản khác nhau của 'giải thích' ... –

7

Bạn có thể sử dụng thư viện pipes của mình, cung cấp các tóm tắt cấp cao hơn để làm việc với các monads miễn phí.

pipes sử dụng monads tự do để cụ thể hóa từng phần của việc tính toán:

  • Các Producer dữ liệu (tức cập nhật của bạn) là một đơn nguyên miễn phí
  • Các Consumer dữ liệu (tức là thông dịch viên của bạn) là một miễn phí đơn nguyên
  • các Pipe dữ liệu (ví dụ logger của bạn) là một đơn nguyên miễn phí

trong thực tế, chúng không phải là ba sepa tỷ lệ miễn phí monads: họ là tất cả cùng một monad miễn phí trong ngụy trang. Sau khi bạn xác định cả ba trong số chúng, bạn kết nối chúng bằng cách sử dụng thành phần đường ống, (>->), để bắt đầu phát trực tuyến dữ liệu.

Tôi sẽ bắt đầu với một phiên bản sửa đổi một chút ví dụ của bạn mà bỏ qua lớp kiểu bạn đã viết:

{-# LANGUAGE RankNTypes #-} 

import Control.Lens 
import Control.Proxy 
import Control.Proxy.Trans.State 
import Control.Monad.Trans.Writer 

data Order = Order deriving (Show) 
data Damage = Damage deriving (Show) 

data EntityUpdate 
    = UpdateTime Double 
    | UpdateOrder Order 
    | UpdateDamage Damage 
    deriving (Show) 

Bây giờ những gì chúng tôi làm là xác định một Update là một Producer của EntityUpdate s:

type Update r = forall m p . (Monad m, Proxy p) => Producer p EntityUpdate m r 

Sau đó, chúng tôi xác định các lệnh thực tế. Mỗi lệnh mang lại bản cập nhật tương ứng bằng cách sử dụng nguyên thủy ống respond, gửi dữ liệu xuống hạ lưu để xử lý.

updateTime :: Double -> Update() 
updateTime t = respond (UpdateTime t) 

updateOrder :: Order -> Update() 
updateOrder o = respond (UpdateOrder o) 

updateDamage :: Damage -> Update() 
updateDamage d = respond (UpdateDamage d) 

Từ một Producer là một đơn nguyên miễn phí, chúng tôi có thể lắp ráp nó bằng cách sử do ký hiệu giống như bạn đã làm cho test chức năng của bạn:

test ::() -> Update() 
-- i.e.() -> Producer p EntityUpdate m() 
test() = runIdentityP $ do 
    updateTime 8.0 
    updateOrder Order 
    updateDamage Damage 
    updateTime 4.0 
    updateDamage Damage 
    updateTime 6.0 
    updateOrder Order 
    updateTime 8.0 

Tuy nhiên, chúng ta có thể cụ thể hóa các thông dịch viên như một Consumer dữ liệu , quá. Điều này là tốt đẹp bởi vì chúng tôi sau đó có thể trực tiếp lớp trên nhà nước trên thông dịch viên thay vì sử dụng các lớp học Entity bạn xác định.

tôi sẽ sử dụng một nhà nước đơn giản:

data MyState = MyState { _numOrders :: Int, _time :: Double, _health :: Int } 
    deriving (Show) 

begin :: MyState 
begin= MyState 0 0 100 

... và xác định một số ống kính thuận tiện cho rõ ràng:

numOrders :: Lens' MyState Int 
numOrders = lens _numOrders (\s x -> s { _numOrders = x}) 

time :: Lens' MyState Double 
time = lens _time (\s x -> s { _time = x }) 

health :: Lens' MyState Int 
health = lens _health (\s x -> s { _health = x }) 

... và bây giờ tôi có thể định nghĩa một thông dịch viên stateful:

eval :: (Proxy p) =>() -> Consumer (StateP MyState p) EntityUpdate IO r 
eval() = forever $ do 
    entityUpdate <- request() 
    case entityUpdate of 
     UpdateTime tDiff -> modify (time  +~ tDiff) 
     UpdateOrder _  -> modify (numOrders +~ 1 ) 
     UpdateDamage _  -> modify (health -~ 1 ) 
    s <- get 
    lift $ putStrLn $ "Current state is: " ++ show s 

Điều đó làm cho việc thông dịch viên rõ ràng hơn nhiều. Chúng ta có thể thấy trong nháy mắt cách nó xử lý các giá trị đến một cách có trạng thái.

Để kết nối của chúng tôi ProducerConsumer chúng tôi sử dụng các nhà điều hành (>->) thành phần, tiếp theo là runProxy, mà biến đổi đường ống của chúng tôi trở lại đơn nguyên cơ sở:

main1 = runProxy $ evalStateK begin $ test >-> eval 

... trong đó sản xuất các kết quả sau:

>>> main1 
Current state is: MyState {_numOrders = 0, _time = 8.0, _health = 100} 
Current state is: MyState {_numOrders = 1, _time = 8.0, _health = 100} 
Current state is: MyState {_numOrders = 1, _time = 8.0, _health = 99} 
Current state is: MyState {_numOrders = 1, _time = 12.0, _health = 99} 
Current state is: MyState {_numOrders = 1, _time = 12.0, _health = 98} 
Current state is: MyState {_numOrders = 1, _time = 18.0, _health = 98} 
Current state is: MyState {_numOrders = 2, _time = 18.0, _health = 98} 
Current state is: MyState {_numOrders = 2, _time = 26.0, _health = 98} 

Bạn có thể thắc mắc tại sao chúng tôi phải thực hiện việc này theo hai bước.Tại sao không chỉ loại bỏ phần runProxy?

Lý do tại sao chúng tôi có thể soạn nhiều hơn hai thứ. Ví dụ: chúng tôi có thể dễ dàng chèn một giai đoạn ghi nhật ký ở giữa testeval. Tôi gọi đó là các giai đoạn trung gian Pipe s:

logger 
    :: (Monad m, Proxy p) 
    =>() -> Pipe p EntityUpdate EntityUpdate (WriterT String m) r 
logger() = runIdentityP $ forever $ do 
    entityUpdate <- request() 
    lift $ tell $ case entityUpdate of 
     UpdateTime t -> "Simulating time for " ++ show t ++ " seconds.\n" 
     UpdateOrder o -> "Giving an order.\n" 
     UpdateDamage d -> "Applying damage.\n" 
    respond entityUpdate 

Một lần nữa, chúng ta có thể thấy rất rõ những gì logger làm: Nó request là một giá trị, tell là một đại diện của các giá trị, và sau đó vượt qua giá trị hơn nữa hạ lưu sử dụng respond.

Chúng tôi có thể chèn số này vào giữa testlogger. Điều duy nhất chúng ta phải nhận thức được rằng tất cả các khâu phải có đơn nguyên cơ sở tương tự, vì vậy chúng tôi sử dụng raiseK để chèn một lớp WriterT cho eval để nó phù hợp với đơn nguyên cơ sở của logger:

main2 = execWriterT $ runProxy $ evalStateK begin $ 
    test >-> logger >-> raiseK eval 

... tạo ra kết quả sau:

>>> main2 
Current state is: MyState {_numOrders = 0, _time = 8.0, _health = 100} 
Current state is: MyState {_numOrders = 1, _time = 8.0, _health = 100} 
Current state is: MyState {_numOrders = 1, _time = 8.0, _health = 99} 
Current state is: MyState {_numOrders = 1, _time = 12.0, _health = 99} 
Current state is: MyState {_numOrders = 1, _time = 12.0, _health = 98} 
Current state is: MyState {_numOrders = 1, _time = 18.0, _health = 98} 
Current state is: MyState {_numOrders = 2, _time = 18.0, _health = 98} 
Current state is: MyState {_numOrders = 2, _time = 26.0, _health = 98} 
"Simulating time for 8.0 seconds.\nGiving an order.\nApplying damage.\nSimulating time for 4.0 seconds.\nApplying damage.\nSimulating time for 6.0 seconds.\nGiving an order.\nSimulating time for 8.0 seconds.\n" 

pipes được thiết kế để giải quyết chính xác loại vấn đề bạn mô tả. Rất nhiều thời gian chúng tôi muốn thống nhất không chỉ DSL tạo ra dữ liệu, mà cả các phiên dịch và các giai đoạn xử lý trung gian nữa. pipes xử lý tất cả các khái niệm này giống hệt nhau và mô hình tất cả chúng thành DSL đường truyền có thể kết nối. Điều này làm cho nó rất dễ dàng để trao đổi trong và ngoài các hành vi khác nhau mà không cần phải xác định khuôn khổ thông dịch viên tùy chỉnh của riêng bạn.

Nếu bạn chưa quen với đường ống, bạn có thể muốn xem tutorial.

+0

Tôi chưa từng gặp 'ống' trước đây - trông thực sự rất tuyệt vời. Tôi bây giờ có kế hoạch dành thời gian để hiểu nó đúng cách. Ai có thể viết thông dịch viên bằng cách sử dụng monads thuần túy mà không có IO? Tuy nhiên, tôi nghĩ rằng chức năng đầy đủ của 'ống' hơi nặng để được * chính xác * những gì tôi đang tìm kiếm, mà tôi sẽ nói là giống như một cơ sở lý thuyết tối thiểu để sử dụng các monads miễn phí để tách mối quan tâm theo cách này. Tôi sẽ có một cái nhìn kỹ lưỡng về ví dụ của bạn, và cũng tại cách 'ống' được xây dựng. –

+0

Các đơn nguyên cơ bản có thể là bất cứ điều gì, bao gồm một nguyên đơn 'State' monad nếu bạn không cần phải sử dụng' IO'. 'ống' thực sự là thư viện coroutine nhẹ nhất. Thành phần chỉ là [5 dòng mã] (http://hackage.haskell.org/packages/archive/pipes/3.2.0/doc/html/src/Control-Proxy-Core-Fast.html) và mọi thứ khác là chỉ cần thực hiện lại một đơn nguyên miễn phí hiệu quả hơn bằng cách sử dụng các quy tắc viết lại. Lý do nó cung cấp nhiều tính năng hơn là tôi đã dành rất nhiều thời gian để tìm kiếm sự trừu tượng đúng đắn. –

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