-
Notifications
You must be signed in to change notification settings - Fork 21
Local state/located effects? #8
Description
After seeing the ICFP presentation on HasChor, I thought it might be nice to discuss it in the upcoming Unfolder episode (coming Wednesday).
Motivation
As an example, I wanted to make a simple fileserver, where the client can send filepaths and the server responds with the contents of those files. The example in the repo use readLine for the client to interact with the user, but I wanted to abstract from this, and have the client be a thread that takes instructions through an MVar; this means the client needs some state. Similar, the server needs some state too: it needs to know which directory to use as the root directory to serve files from.
Not so nice solution
However, I couldn't really see an easy way to pass this state to the choreography. I can't take the two state parameters as arguments to the choreography, because the server should not need the client state and vice versa. I can take two located parameters
choreography ::
FilePath @ "server"
-> (MVar Instruction, MVar Lazy.ByteString) @ "client"
-> Choreo IO ()and pass Empty for the argument that is unused:
clientProcess =
runChoreography
cfg
(choreography Empty (Wrap (instrVar, resultsVar)))
"client"but this doesn't feel terribly clean.
Better abstraction?
I tried coming up with an abstraction that makes this a little cleaner. First, hoist:
hoistChoreo :: forall m n.
(forall l a. KnownSymbol l => Proxy l -> m a -> n a)
-> (forall a. Choreo m a -> Choreo n a)
hoistChoreo hoist = go
where
go :: Choreo m a -> Choreo n a
go (Return x) = Return x
go (Do eff k) =
case eff of
Local l f -> Do (Local l (\un -> hoist l (f un))) (go . k)
Comm s x r -> Do (Comm s x r) (go . k)
Cond l x f -> Do (Cond l x (go . f)) (go . k)(this definition is uncontroversial, I think). Then, since the only way to extend the Choreo monad is by picking a different underlying m, I defined
data LocalReaderSig f m a where
LocalLift :: m a -> LocalReaderSig f m a
LocalAsk :: KnownSymbol l => Proxy l -> LocalReaderSig f m (f l)
type LocalReader f m = Freer (LocalReaderSig f m)
liftLocal :: m a -> LocalReader f m a
liftLocal = toFreer . LocalLift
askLocal ::
KnownSymbol l
=> Proxy l -> Choreo (LocalReader f m) (f l @ l)
askLocal l = locally l (\_un -> toFreer (LocalAsk l))this then makes it possible to "provide local state", in a somewhat similar fashion to endpoint projection:
hoistLocal :: forall m f l.
(KnownSymbol l, Monad m)
=> Proxy l
-> f l
-> (forall a. Choreo (LocalReader f m) a -> Choreo m a)
hoistLocal l s = hoistChoreo hoist
where
hoist :: KnownSymbol l' => Proxy l' -> LocalReader f m a -> m a
hoist l' = interpFreer $ \case
LocalLift act -> act
LocalAsk l'' ->
case sameSymbol l' l'' of
Nothing -> error "invalid"
Just Refl ->
case sameSymbol l' l of
Nothing -> error "irrelevant"
Just Refl -> return sThe choreography is now a little cleaner:
data LocalState :: Symbol -> Type where
ServerState :: FilePath -> LocalState "server"
ClientState :: MVar Instruction -> MVar Lazy.ByteString -> LocalState "client"
choreography :: Choreo (LocalReader LocalState IO) ()
choreography = do
serverState <- askLocal server
clientState <- askLocal client
...It still doesn't feel terribly satisfactory though. Am I missing a more obvious solution?
Proper solution?
It seems to me that the cleaner solution would if the argument to Choreo is an indexed monad of kind Symbol -> Type -> Type. There is no reason to assume that the client and the server require the exact same of effects, after all -- indeed, it seems quite likely that they will not.