-
Notifications
You must be signed in to change notification settings - Fork 52
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
Conversation
There was a problem hiding this 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'. |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
>>=
on trees is substitution.
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. ✨ |
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
{-# 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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! ❤️
The type of |
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.