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

Binary nondeterminism #197

Merged
merged 40 commits into from
Sep 11, 2019
Merged

Binary nondeterminism #197

merged 40 commits into from
Sep 11, 2019

Conversation

robrix
Copy link
Contributor

@robrix robrix commented Aug 25, 2019

This PR redefines NonDetC, changing it from a Church-encoded list to a Church-encoded binary tree. This should result in greater preservation of parallelism between branches, tho I haven’t actually measured that. It should also result in slightly simpler Core since e.g. pure no longer needs to invoke both continuations, but again, I haven’t measured that.

Copy link
Contributor Author

@robrix robrix left a comment

Choose a reason for hiding this comment

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

Ready for review.

l <|> r = CullC $ ReaderC $ \ cull ->
if cull then
NonDetC $ \ fork leaf nil ->
runNonDetC (runReader cull (runCullC l)) fork leaf (runNonDetC (runReader cull (runCullC r)) fork leaf nil)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When we’re inside a cull, we alternate by using the rhs as the nil continuation for the lhs.

NonDetC $ \ fork leaf nil ->
runNonDetC (runReader cull (runCullC l)) fork leaf (runNonDetC (runReader cull (runCullC r)) fork leaf nil)
else
runReader cull (runCullC l) <|> runReader cull (runCullC r)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When we aren’t inside a cull, then we just alternate as normal for the underlying NonDetC.

{-# INLINE fail #-}

instance MonadFix m => MonadFix (CutC m) where
mfix f = CutC (\ cons nil _ -> mfix (\ a -> runCutC (f (head a)) (fmap . (:)) (pure []) (pure [])) >>= foldr cons nil)
{-# INLINE mfix #-}

instance MonadIO m => MonadIO (CutC m) where
liftIO io = CutC (\ cons nil _ -> liftIO io >>= flip cons nil)
liftIO io = lift (liftIO io)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I attempted to apply the same approach to CutC, but (much to my surprise) the semantics of cut/call are such that you actually want the left-to-right sequencing of the encoded list formulation. I’m not yet convinced that it can’t be done with a binary tree formulation, but it sure isn’t straightforward.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you walk me through what’s going on here? I’m not sure I follow the operative difference introduced by this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The semantics are unchanged, because:

lift m = CutC (\ cons nil _ -> m >>= flip cons nil)

thus,

lift (liftIO io) = CutC (\ cons nil _ -> liftIO io >>= flip cons nil)

which is exactly the definition we’ve replaced.

deriving (Functor, Generic1)

instance HFunctor NonDet
instance Effect NonDet
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I honestly don’t think we should use DeriveAnyClass for these, as it’s a false economy.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Up to you. I personally like it, but given that it’s a two-line difference I’m not worked up about it.



-- | Run a 'NonDet' effect, collecting all branches’ results into an 'Alternative' functor.
--
-- Using '[]' as the 'Alternative' functor will produce all results, while 'Maybe' will return only the first. However, unlike 'runNonDetOnce', this will still enumerate the entire search space before returning, meaning that it will diverge for infinite search spaces, even when using 'Maybe'.
-- Using @[]@ as the 'Alternative' functor will produce all results, while 'Maybe' will return only the first. However, unlike 'Control.Effect.Cull.runNonDetOnce', this will still enumerate the entire search space before returning, meaning that it will diverge for infinite search spaces, even when using 'Maybe'.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Haddock can’t link to [] because [] isn’t defined in (Haskell) source. Syntax sugar!

{-# INLINE liftIO #-}

instance MonadPlus (NonDetC m)

instance MonadTrans NonDetC where
lift m = NonDetC (\ cons nil -> m >>= flip cons nil)
lift m = NonDetC (\ _ leaf _ -> m >>= leaf)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We no longer invoke the nil continuation for pure & lift, which should make this slightly more efficient in general.


instance Alternative BinaryTree where
empty = Nil
(<|>) = Fork
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need the Alternative instance because runNonDet (used in eff) requires it. cf #207.

{-# INLINE eff #-}


data BinaryTree a = Nil | Leaf a | Fork (BinaryTree a) (BinaryTree a)
deriving (Eq, Foldable, Functor, Ord, Show, Traversable)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’ve opted not to export this for the time being, but I feel a bit weird about that. I don’t know that I really want to add a Data.BinaryTree module, either, since that seems like a potentially contended name for the module. Tho perhaps I worry too much, since hoogle doesn’t seem to know about any such modules?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don’t object to keeping it hidden, personally. The more details we hide, the more we can iterate on the internals without breaking public API. We’re already entailing a good bit of churn these days, after all!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is an excellent point.


instance Applicative BinaryTree where
pure = Leaf
f <*> a = fold Fork (<$> a) Nil f
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This and the definition of >>= make me extremely happy.

(<|>) = Fork

instance Monad BinaryTree where
a >>= f = fold Fork f Nil a
Copy link
Contributor Author

Choose a reason for hiding this comment

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

>>= on trees is substitution.

@robrix robrix marked this pull request as ready for review September 3, 2019 15:36
@patrickt
Copy link
Collaborator

patrickt commented Sep 8, 2019

Informal first observations based off of #211 indicate this gives a slight speedup, which is further improved by marking the BinaryTree Functor/Monad/etc. methods as INLINE. ✨

@robrix
Copy link
Contributor Author

robrix commented Sep 8, 2019

That’s super exciting!

@@ -51,18 +52,20 @@ cull m = send (Cull m pure)
--
-- prop> run (runNonDet (runCull (pure a <|> pure b))) === [a, b]
runCull :: Alternative m => CullC m a -> m a
runCull (CullC m) = runNonDetC (runReader False m) ((<|>) . pure) empty
runCull (CullC m) = runNonDetC (runReader False m) (<|>) pure empty
Copy link
Collaborator

Choose a reason for hiding this comment

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

I like that this got simpler.

deriving (Functor, Generic1)

instance HFunctor NonDet
instance Effect NonDet
Copy link
Collaborator

Choose a reason for hiding this comment

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

Up to you. I personally like it, but given that it’s a two-line difference I’m not worked up about it.

src/Control/Effect/NonDet.hs Show resolved Hide resolved
{-# INLINE fail #-}

instance MonadFix m => MonadFix (CutC m) where
mfix f = CutC (\ cons nil _ -> mfix (\ a -> runCutC (f (head a)) (fmap . (:)) (pure []) (pure [])) >>= foldr cons nil)
{-# INLINE mfix #-}

instance MonadIO m => MonadIO (CutC m) where
liftIO io = CutC (\ cons nil _ -> liftIO io >>= flip cons nil)
liftIO io = lift (liftIO io)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you walk me through what’s going on here? I’m not sure I follow the operative difference introduced by this change.

@robrix robrix requested a review from patrickt September 9, 2019 20:35
@robrix
Copy link
Contributor Author

robrix commented Sep 9, 2019

hypnotoad on ice

@robrix robrix added this to the 0.6 milestone Sep 10, 2019
Copy link
Collaborator

@patrickt patrickt left a comment

Choose a reason for hiding this comment

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

Wonderful! One last nitpick: I would mark the changelog entry for this patch as backwards-compatible. Otherwise, well done! ❤️

@robrix
Copy link
Contributor Author

robrix commented Sep 11, 2019

I would mark the changelog entry for this patch as backwards-compatible.

The type of NonDetC & runNonDetC changed, and both are exported, so technically it’s not, alas.

@robrix robrix merged commit 963ad9f into master Sep 11, 2019
@robrix robrix deleted the binary-nondet branch September 11, 2019 01:43
@robrix robrix mentioned this pull request Oct 23, 2019
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants