Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update docs for runTH and bindTH #394

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 47 additions & 28 deletions src/Polysemy/Internal/Tactics.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@ import Polysemy.Internal.Union
-- effect @Tactics@, which is capable of rewriting monadic actions so they run
-- in the correct stateful environment.
--
-- Inside a 'Tactical', you're capable of running 'pureT', 'runT' and 'bindT'
-- which are the main tools for rewriting monadic stateful environments.
-- The @f@ type here is existential and corresponds to "whatever
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the "existential" comment back here.

-- state the other effects want to keep track of." @f@ is always
-- a 'Functor'.
--
-- Inside a 'Tactical', you're capable of running 'pureT', 'runTSimple', 'bindTSimple',
-- 'runT' and 'bindT', which are the main tools for rewriting monadic stateful environments.
--
-- You should usually reach for 'runTSimple' and 'bindTSimple' first, as they are easier to use,
-- albeit less powerful versions of 'runT' and 'bindT'.
--
-- For example, consider trying to write an interpreter for
-- 'Polysemy.Resource.Resource', whose effect is defined as:
Expand All @@ -38,41 +45,53 @@ import Polysemy.Internal.Union
-- data 'Polysemy.Resource.Resource' m a where
-- 'Polysemy.Resource.Bracket' :: m a -> (a -> m ()) -> (a -> m b) -> 'Polysemy.Resource.Resource' m b
-- @
--
-- Here we have an @m a@ which clearly needs to be run first, and then
-- subsequently call the @a -> m ()@ and @a -> m b@ arguments. In a 'Tactical'
-- environment, we can write the threading code thusly:
-- We can interpret this effect like so:
--
-- @
-- 'Polysemy.Resource.Bracket' alloc dealloc use -> do
-- alloc' <- 'runT' alloc
-- dealloc' <- 'bindT' dealloc
-- use' <- 'bindT' use
-- runResource = Polysemy.interpretH $ \\case
-- 'Polysemy.Resource.Bracket' alloc dealloc use -> do
-- resource <- 'runTSimple' alloc
-- result <- 'bindTSimple' use resource
-- 'bindTSimple' dealloc resource
Copy link
Collaborator

@KingoftheHomeless KingoftheHomeless Nov 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong, since this will have dealloc inherit the state from alloc and not result.
The "proper" solution would be:

  fresource <- runTSimple alloc
  fresourceresult <- bindTSimple (\a -> ((,) a) <$> use a) fresource
  bindTSimple (\(resource, result) ->  result <$ dealloc resource) fresourceresult

This is very complicated. Also, this documentation is misleading since this isn't actually how runResource is implemented; it does some extra stuff like using the inspector to check if use failed.

All in all, we should pick a different example than Resource.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, does the current example do the same thing regarding the "state inheritance"? This is the impression I was left with (and it also seems to be the point of the example), from the current documentation.

I guess it would be misleading if the actual runResource runs in a different way.

-- pure result
-- @
--
-- where
-- To run embedded higher-order actions (e.g. @alloc :: m a@s) we use 'runTSimple'.
--
-- @
-- alloc' :: 'Polysemy.Sem' ('Polysemy.Resource.Resource' ': r) (f a1)
-- dealloc' :: f a1 -> 'Polysemy.Sem' ('Polysemy.Resource.Resource' ': r) (f ())
-- use' :: f a1 -> 'Polysemy.Sem' ('Polysemy.Resource.Resource' ': r) (f x)
-- @
-- If we have a kleisli action (e.g. @use :: a -> m b@) we can instead use 'bindTSimple'.
--
-- The @f@ type here is existential and corresponds to "whatever
Copy link
Member Author

@googleson78 googleson78 Nov 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately there is no long an example to attach this talking point to, and it seems rather important, as it explains the that a forall f. Functor f => f ... is being used as "state".

Is there some other example that can be given here to demonstrate this? @KingoftheHomeless @tek @TheMatten

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we just put it at the end of the doc comment, so that the association with the type decl is apparent?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok obviously that should be "top of the comment" 😄
in any case, the first sentence mentions "threading the state", so it might be sensible to just expand on that there

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added it back near the top + another piece of documentation that I previously deleted

-- state the other effects want to keep track of." @f@ is always
-- a 'Functor'.
-- Note that because of the types of @'bindTSimple' use resource@ and @'bindTSimple' dealloc resource@
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also add this "same environment" comment back here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haddock rendering doesn't do a great job of distinguishing @@ blocks from normal text..

-- both use @f a@, they must run in the same stateful environment. This
-- means, for illustration, any 'Polysemy.State.put's run inside the @'bindTSimple' use resource@
-- block will not be visible inside of the @dealloc@ block.
--
-- @alloc'@, @dealloc'@ and @use'@ are now in a form that can be
-- easily consumed by your interpreter. At this point, simply bind
-- them in the desired order and continue on your merry way.
-- There are however some cases where the @\*Simple@ functions are not sufficient.
--
-- We can see from the types of @dealloc'@ and @use'@ that since they both
-- consume a @f a1@, they must run in the same stateful environment. This
-- means, for illustration, any 'Polysemy.State.put's run inside the @use@
-- block will not be visible inside of the @dealloc@ block.
-- Consider, for example, the 'Polysemy.Reader.local' action.
--
-- In it, we want any inner computations to be executed with a __modified__ environment.
--
-- This is not possible with 'runTSimple' and 'bindTSimple', because they automatically
-- take the care to run all your embedded actions by using the current set of interpreters.
--
-- Instead, we need to use 'runT' and 'bindT', which give more control over how inner actions are run.
--
-- As an example, let's look at the implementation for running a 'Polysemy.Reader.Reader'
-- with a constant value:
--
-- @
-- 'Polysemy.Reader.runReader' i = 'Polysemy.interpretH' $ \\case
-- 'Polysemy.Reader.Ask' -> pureT i
-- 'Polysemy.Reader.Local' f m -> do
-- mm <- 'runT' m
-- 'Polysemy.raise' $ 'Polysemy.Reader.runReader' (f i) mm
-- @
--
-- We can see that there is a recursive call to 'Polysemy.Reader.runReader', which handles
-- any other potential 'Polysemy.Reader' computations that might be present in our @m@.
--
-- Power users may explicitly use 'getInitialStateT' and 'bindT' to construct
-- whatever data flow they'd like; although this is usually unnecessary.
-- The thing to note here is that the call is made with a __modified__ environment of @f i@
-- instead of the __initial__ @i@ that our interpreter was currently running with.
type Tactical e m r x = ∀ f. Functor f
=> Sem (WithTactics e f m r) (f x)

Expand Down