-
Notifications
You must be signed in to change notification settings - Fork 4
Stream fusion #27
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
base: main
Are you sure you want to change the base?
Stream fusion #27
Conversation
src/Control/Monad/Logic/Sequence.hs
Outdated
| , getSeq | ||
| #endif | ||
| , View(..) | ||
| , alt |
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'm not sure if I actually want to export alt, asumSeqT, and chooseSeqT. I think I mainly added them to test things when rewrites were not working for me.
| -- so as to be more efficient in the face of deeply left-associated `<|>` or | ||
| -- `mplus` applications. | ||
| newtype SeqT m a = SeqT (Queue (m (View m a))) | ||
| newtype SeqT m a = SeqT { runSeqT :: (Queue (m (View m 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.
I don't think I ended up using runSeqT in code, but I keep adding it because generally I feel like it's nice to have named projections like this.
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.
Wouldn't unSeqT be more idiomatic these days? I'm not sure there's much point adding a record selector like that for an internal type, but it's your call of course.
|
|
||
| {-# RULES | ||
| "stream-unstream" [2] forall s. stream (unstream s) = s; | ||
| "toView-fromView" [1] forall s. fromView (toView s) = s; |
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 should probably remove this one
|
Let me finish up my current PR, which adds instances and some basic tests, though not as many as it needs. Then I'll happily take a closer look at this PR and then hopefully add some more tests before release. |
|
|
||
| done :: Monad m => StreamM m a | ||
| done = StreamM (const (return Done)) Empty | ||
| {-# INLINE CONLIKE [0] done #-} |
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 think CONLIKE is justifiable here given that everything here is non-polymorphic and concretely specified. So no surprises right?
| Done -> Skip (Boundary s_a) | ||
| {-# INLINE[1] bind_s #-} | ||
| {- | ||
| bind_s :: Monad m => StreamM m a -> (a -> StreamM m b) -> StreamM m b |
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.
oh and I can delete this.
|
How is that concrete? It has a Monad constraint!
…On Mon, Jul 26, 2021, 2:36 PM Jason Dagit ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In src/Control/Monad/Logic/Sequence/Internal.hs
<#27 (comment)>:
> - Empty -> return Empty
- h :< t -> f h `altView` (t >>= f)
-
- {-# INLINEABLE (>>) #-}
- (toView -> m) >> n = fromView $ m >>= \x -> case x of
- Empty -> return Empty
- _ :< t -> n `altView` (t >> n)
+ (>>=) = bind
+
+bind :: Monad m => SeqT m a -> (a -> SeqT m b) -> SeqT m b
+bind m f = unstream (bind_s (stream m) (stream . f))
+{-# INLINE[3] bind #-}
+
+done :: Monad m => StreamM m a
+done = StreamM (const (return Done)) Empty
+{-# INLINE CONLIKE [0] done #-}
I think CONLIKE is justifiable here given that everything here is
non-polymorphic and concretely specified. So no surprises right?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#27 (review)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAOOF7JFXDCTKQ7EOZNHCFDTZWTJZANCNFSM5BAUVYLQ>
.
|
As it turns out, I can't use a rewrite rule to replace |
|
I just pushed a new version that should be cleaner. |
| , observe | ||
| , observeMaybeT | ||
| , observeMaybe | ||
| , chooseSeqT |
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.
Is there a better name for this? The only reason we provide it as a "primitive" is so we can write it in a way that take advantage of the internals, but maybe choose (without the SeqT) is a better name?
| stream m = StreamM next m where | ||
| {-# INLINE next #-} | ||
| next s = do | ||
| x <- toView s |
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.
My kingdom for an <$>....
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 think it's better this way. fmap would introduce laziness I'm guessing we don't want.
|
Maybe I should try writing |
|
I still haven't read this PR. Will try to do so shortly, but also want to
work on my own!
…On Mon, Jul 26, 2021, 3:56 PM Jason Dagit ***@***.***> wrote:
Maybe I should try writing fmap_s and making that fusable too. Any other
functions in here that could benefit from it?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#27 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAOOF7P4JOE56EVIY4JIKXTTZW4VJANCNFSM5BAUVYLQ>
.
|
treeowl
left a comment
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'm not really convinced yet.
| Empty -> go t | ||
| hi :< SeqT ti -> return (hi :< SeqT (ti S.>< t)) | ||
| {-# INLINE[0] toView #-} | ||
|
|
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.
What's go for? Can you just use INLINABLE[0] with the original?
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 get a little fuzzy on which things I individually benchmarked, but I would have sworn writing it this way generates slightly faster code and it's just a difference of one line, right?
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.
It's the least of my worries.
| stream m = StreamM next m where | ||
| {-# INLINE next #-} | ||
| next s = do | ||
| x <- toView s |
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 think it's better this way. fmap would introduce laziness I'm guessing we don't want.
| {-# INLINE altView #-} | ||
| alt :: Monad m => SeqT m a -> SeqT m a -> SeqT m a | ||
| alt a b = unstream (alt_s (stream a) (stream b)) | ||
| {-# INLINE[3] alt #-} |
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'm quite concerned that this approach may cause asymptotic performance trouble in the face of lots of <|> (associated various ways, and especially recursive).
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.
Yeah, I'm curious about that case too. My simple benchmark does an iterative deepening search. And then I convert the generated values to a streamly stream. That conversion can either be done using something like observeAll or msplit with unfoldrM. It's the program where I originally discovered the quadratic slowdown in LogicT. So I've been using that to get a sense of whether our <|> implementation is dealing well with associativity.
For the version here there's no runtime difference between using msplit vs. observeAll for SeqT. Have you looked in bench? Do any of those cover the case you are imagining? It's a little bit hard to get a graph from those benchmarks so I haven't been running those between changes (also they take forever to run). But I would like to be able to answer question like this using benchmarks as a sort of ground truth on runtime.
|
I haven't read everything, but this proposal smells like a streamy version of the classic newtype ML m a = ML (m (View (ML m) a))Isn't the reflection-without-remorse improvement to |
Not sure what you mean by classic here. Is there something you could point me to?
If you look through the branch history, you'll probably see that this was the type I tried storing in the stream at first. I ran into some issues with it and when dmwit started helping me we rewrite it to the version you see now.
I think that's kind of the point? For the parts that fuse we can use the linearized stream computation and for places where fusion doesn't happen we'd need to fallback on the toView/fromView version (which isn't included in my branch, but see #30). And I was thinking that should preserve the asymptotic improvements. I could be wrong, but that was my mental model for how fusion would work. |
|
The sort of thing I'm worried about is a recursive function that generates things like ((f 1 <|> f 2) <|> (f 3 <|> f 4)) <|>
((f 5 <|> f 6) <|> (f 7 <|> f 8)) to an arbitrary depth. What will your code do with that? The whole point of reflection without remorse is to make sure there are no pathologically bad cases. Now it could be that your approach can be tweaked to ensure that, but from what I understand so far I don't think it does yet. |
|
Previous comment has been edited to fix numbers. |
|
Consider also what happens if someone alternates between |
|
What I mean by that last: boffin :: Monad m => SeqT m a -> SeqT m a
boffin m = do
Just (_, m') <- msplit m
m' <|> m'
grump :: Monad m => Int -> SeqT m a -> SeqT m a
grump n m = iterate boffin m !! n |
|
Hmmmmmm.... I haven't been able to make this fall over yet. Interesting.... |
|
OK, I was testing wrong. I found an example. This runs in very little time with the current implementation, but takes longer than I was willing to wait with the stream implementation. I'm pretty sure this example could be simplified a bit, but you should get the gist. {-# language BangPatterns #-}
{-# language LambdaCase #-}
module Main(main) where
import Control.Monad.Logic.Sequence
import Control.Monad.Logic.Class
import Control.Applicative
import Control.Monad.Fail
import Data.Foldable (foldl')
import Data.Bifunctor
grump :: MonadLogic m => Int -> m a -> m a
grump n m = foldl' (\acc _ -> acc <|> m <|> acc) empty [1..n]
msplitN :: MonadLogic m => Int -> m a -> m ([a], m a)
msplitN n !m | n <= 0 = pure ([], empty)
msplitN n m = msplit m >>= \case
Nothing -> pure ([], empty)
Just (a, m') -> first (a :) <$> msplitN (n - 1) m'
main :: IO ()
main = do
r <- observeT . fmap fst . msplitN 10000 $ grump 1000000 (pure (20 :: Int))
print $ last r |
This is in Figure 7 in the Reflection without Remorse paper. They explain that it works great for |
I see. I'm sad you found a counter example but also glad you found a counter example. We should make sure something like this is in the benchmark suite. In my local benchmark, and part of the reason I've been slow to convert it, is I convert the values to streamly streams because they have a nice batching API. I can then time batches. If each batch takes roughly the same amount of time I know we haven't introduced asymptotic factors but if the time between batches increases I know I'm in trouble. |
|
Oh, I forgot to say this in my last message. The big speedup happened when I made
|
|
It sounds plausible, since |
|
I had just been testing this branch with my benchmark. I wish I had thought to test a few simple uses of bind in the repl. It's completely bogus. |
Aside from the lack of tests, do you have any objections to me merging this? @treeowl
As an aside, I see tests as a requirement for the next release. So they're definitely on my mind. I've just been sort of obsessed with closing the performance gap. Although, if we can reliably be within 2x of
LogicTthat's close enough for now.