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
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8edd680
Lift MonadFail/MonadIO using MonadTrans.
robrix Aug 22, 2019
eb45dda
Test that empty is handled correctly within cull.
robrix Aug 24, 2019
91be10b
Fix the doc reference to runNonDetOnce.
robrix Aug 24, 2019
162d626
Fix the doc reference to [].
robrix Aug 24, 2019
1900a53
Add some simple tests of Applicative.
robrix Aug 24, 2019
f3c830f
Add a simple test of Alternative.
robrix Aug 24, 2019
83cbf16
Define a datatype of binary trees.
robrix Aug 24, 2019
c80d652
Derive some instances for B.
robrix Aug 24, 2019
991e55c
Define an Applicative instance for B.
robrix Aug 24, 2019
34a9979
Define an Alternative instance for B.
robrix Aug 24, 2019
8359300
Define a Monad instance for B.
robrix Aug 24, 2019
6bd09c1
Rename B to Bin.
robrix Aug 24, 2019
5ec21db
Derive some more instances for Bin.
robrix Aug 24, 2019
ae79dad
Export Bin.
robrix Aug 24, 2019
a37b26e
Define a fold over Bin.
robrix Aug 24, 2019
b72a107
Test that NonDetC preserves branching structure.
robrix Aug 24, 2019
c53a119
Define NonDetC as a church-encoded binary tree.
robrix Aug 24, 2019
20bdb4b
Don’t prime the names in mfix.
robrix Aug 24, 2019
42bdf1b
Simplify the MonadFail & MonadIO instances for CutC using MonadTrans.
robrix Aug 24, 2019
cf3db93
Simplify the MonadTrans instance for NonDetC.
robrix Aug 24, 2019
5ae55a2
Stub in a module for binary trees.
robrix Aug 25, 2019
8d0cf59
Move Bin to its own module.
robrix Aug 25, 2019
acadd3b
Rename Bin to BinaryTree & foldBin to fold.
robrix Aug 25, 2019
6460e72
Spacing.
robrix Aug 25, 2019
b8f9b15
Alignment.
robrix Aug 25, 2019
99b748f
Alignment.
robrix Aug 25, 2019
d2bbd2d
Move BinaryTree back into Control.Effect.NonDet but don’t export it.
robrix Aug 25, 2019
73a8ba3
:fire: a redundant use of GeneralizedNewtypeDeriving.
robrix Aug 25, 2019
725cec1
:fire: uses of DeriveAnyClass & DerivingStrategies.
robrix Aug 25, 2019
dd85cf0
Spacing.
robrix Aug 25, 2019
a7db05e
Merge branch 'master' into binary-nondet
robrix Sep 3, 2019
5674d9b
Try to be consistent with eliminator names.
robrix Sep 3, 2019
0a08f49
Don’t shadow pure.
robrix Sep 3, 2019
e70be43
Define <*> using fold.
robrix Sep 3, 2019
3eabaf1
Define >>= using fold.
robrix Sep 3, 2019
06dbd73
Add a note to the changelog.
robrix Sep 3, 2019
069c9cf
Merge branch 'master' into binary-nondet
robrix Sep 8, 2019
1bb5299
INLINE all the things.
robrix Sep 9, 2019
d4e1967
Merge branch 'master' into binary-nondet
robrix Sep 9, 2019
56d00bf
Merge branch 'master' into binary-nondet
robrix Sep 9, 2019
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
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

- Removes `fmap'` and `handlePure`, both deprecated in 0.5.0.0 ([#205](https://github.com/fused-effects/fused-effects/pull/205)).

- Redefines `NonDetC` as a Church-encoded binary tree instead of a Church-encoded list ([#197](https://github.com/fused-effects/fused-effects/pull/197)).

# v0.5.0.1

- Adds support for ghc 8.8.1.
Expand Down
13 changes: 8 additions & 5 deletions src/Control/Effect/Cull.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ instance Effect Cull where
-- | Cull nondeterminism in the argument, returning at most one result.
--
-- prop> run (runNonDet (runCull (cull (pure a <|> pure b)))) === [a]
-- prop> run (runNonDet (runCull (cull (empty <|> pure a)))) === [a]
-- prop> run (runNonDet (runCull (cull (pure a <|> pure b) <|> pure c))) === [a, c]
-- prop> run (runNonDet (runCull (cull (asum (map pure (repeat a)))))) === [a]
cull :: (Carrier sig m, Member Cull sig) => m a -> m a
Expand All @@ -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.


newtype CullC m a = CullC { runCullC :: ReaderC Bool (NonDetC m) a }
deriving (Applicative, Functor, Monad, Fail.MonadFail, MonadFix, MonadIO)

instance Alternative (CullC m) where
empty = CullC empty
{-# INLINE empty #-}
l <|> r = CullC $ ReaderC $ \ cull -> NonDetC $ \ cons nil -> do
runNonDetC (runReader cull (runCullC l))
(\ a as -> cons a (if cull then nil else as))
(runNonDetC (runReader cull (runCullC r)) cons nil)
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.

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 (<|>) #-}

instance MonadPlus (CullC m)
Expand Down
4 changes: 2 additions & 2 deletions src/Control/Effect/Cut.hs
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,15 @@ instance Monad (CutC m) where
{-# INLINE (>>=) #-}

instance Fail.MonadFail m => Fail.MonadFail (CutC m) where
fail s = CutC (\ _ _ _ -> Fail.fail s)
fail s = lift (Fail.fail s)
{-# 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.

{-# INLINE liftIO #-}

instance MonadPlus (CutC m)
Expand Down
80 changes: 58 additions & 22 deletions src/Control/Effect/NonDet.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{-# LANGUAGE DeriveAnyClass, DeriveFunctor, DeriveGeneric, DerivingStrategies, FlexibleInstances, GeneralizedNewtypeDeriving, MultiParamTypeClasses, RankNTypes, TypeOperators, UndecidableInstances #-}
{-# LANGUAGE DeriveGeneric, DeriveTraversable, FlexibleInstances, MultiParamTypeClasses, RankNTypes, TypeOperators, UndecidableInstances #-}
module Control.Effect.NonDet
( -- * NonDet effect
NonDet(..)
Expand All @@ -12,81 +12,117 @@ module Control.Effect.NonDet
, run
) where

import Control.Applicative (Alternative(..))
import Control.Applicative (Alternative(..), liftA2)
import Control.Effect.Carrier
import Control.Monad (MonadPlus(..))
import Control.Monad (MonadPlus(..), join)
import qualified Control.Monad.Fail as Fail
import Control.Monad.Fix
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Data.Maybe (fromJust)
import GHC.Generics (Generic1)

data NonDet m k
= Empty
| Choose (Bool -> m k)
deriving stock (Functor, Generic1)
deriving anyclass (HFunctor, Effect)
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!

--
-- prop> run (runNonDet (pure a)) === [a]
-- prop> run (runNonDet (pure a)) === Just a
runNonDet :: (Alternative f, Applicative m) => NonDetC m a -> m (f a)
runNonDet (NonDetC m) = m (fmap . (<|>) . pure) (pure empty)
runNonDet (NonDetC m) = m (liftA2 (<|>)) (pure . pure) (pure empty)

-- | A carrier for 'NonDet' effects based on Ralf Hinze’s design described in [Deriving Backtracking Monad Transformers](https://www.cs.ox.ac.uk/ralf.hinze/publications/#P12).
newtype NonDetC m a = NonDetC
{ -- | A higher-order function receiving two parameters: a function to combine each solution with the rest of the solutions, and an action to run when no results are produced.
runNonDetC :: forall b . (a -> m b -> m b) -> m b -> m b
{ -- | A higher-order function receiving three continuations, respectively implementing '<|>', 'pure', and 'empty'.
runNonDetC :: forall b . (m b -> m b -> m b) -> (a -> m b) -> m b -> m b
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 is so much cleaner ✨

}
deriving stock (Functor)
deriving (Functor)

-- $
-- prop> run (runNonDet (pure a *> pure b)) === Just b
-- prop> run (runNonDet (pure a <* pure b)) === Just 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.

These sorts of tests of lawfulness strike me as something we could use more of.

instance Applicative (NonDetC m) where
pure a = NonDetC (\ cons -> cons a)
pure a = NonDetC (\ _ leaf _ -> leaf a)
{-# INLINE pure #-}
NonDetC f <*> NonDetC a = NonDetC $ \ cons ->
f (\ f' -> a (cons . f'))
NonDetC f <*> NonDetC a = NonDetC $ \ fork leaf nil ->
f fork (\ f' -> a fork (leaf . f') nil) nil
{-# INLINE (<*>) #-}

-- $
-- prop> run (runNonDet (pure a <|> (pure b <|> pure c))) === Fork (Leaf a) (Fork (Leaf b) (Leaf c))
-- prop> run (runNonDet ((pure a <|> pure b) <|> pure c)) === Fork (Fork (Leaf a) (Leaf b)) (Leaf c)
instance Alternative (NonDetC m) where
empty = NonDetC (\ _ nil -> nil)
empty = NonDetC (\ _ _ nil -> nil)
{-# INLINE empty #-}
NonDetC l <|> NonDetC r = NonDetC $ \ cons -> l cons . r cons
NonDetC l <|> NonDetC r = NonDetC $ \ fork leaf nil -> fork (l fork leaf nil) (r fork leaf nil)
{-# INLINE (<|>) #-}

instance Monad (NonDetC m) where
NonDetC a >>= f = NonDetC $ \ cons ->
a (\ a' -> runNonDetC (f a') cons)
NonDetC a >>= f = NonDetC $ \ fork leaf nil ->
a fork (\ a' -> runNonDetC (f a') fork leaf nil) nil
{-# INLINE (>>=) #-}

instance Fail.MonadFail m => Fail.MonadFail (NonDetC m) where
fail s = NonDetC (\ _ _ -> Fail.fail s)
fail s = lift (Fail.fail s)
{-# INLINE fail #-}

instance MonadFix m => MonadFix (NonDetC m) where
mfix f = NonDetC (\ cons nil -> mfix (\ a -> runNonDetC (f (head a)) (fmap . (:)) (pure [])) >>= foldr cons nil)
mfix f = NonDetC $ \ fork leaf nil ->
mfix (\ a -> runNonDetC (f (fromJust (fold (<|>) Just Nothing a)))
(liftA2 Fork)
(pure . Leaf)
(pure Nil))
>>= fold 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.

Note that this implementation has the same behaviour as the previous one, and as such is subject to #209. I do not intend to fix that behaviour in this PR.

{-# INLINE mfix #-}

instance MonadIO m => MonadIO (NonDetC m) where
liftIO io = NonDetC (\ cons nil -> liftIO io >>= flip cons nil)
liftIO io = lift (liftIO io)
{-# 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.

{-# INLINE lift #-}

instance (Carrier sig m, Effect sig) => Carrier (NonDet :+: sig) (NonDetC m) where
eff (L Empty) = empty
eff (L (Choose k)) = k True <|> k False
eff (R other) = NonDetC $ \ cons nil -> eff (handle [()] (fmap concat . traverse runNonDet) other) >>= foldr cons nil
eff (R other) = NonDetC $ \ fork leaf nil -> eff (handle (Leaf ()) (fmap join . traverse runNonDet) other) >>= fold fork leaf nil
{-# 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
robrix marked this conversation as resolved.
Show resolved Hide resolved
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.


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.


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.



fold :: (b -> b -> b) -> (a -> b) -> b -> BinaryTree a -> b
fold fork leaf nil = go where
go Nil = nil
go (Leaf a) = leaf a
go (Fork a b) = fork (go a) (go b)


-- $setup
-- >>> :seti -XFlexibleContexts
-- >>> import Test.QuickCheck
Expand Down