-
Notifications
You must be signed in to change notification settings - Fork 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
Faster implementation for Free #104
Conversation
As I look at this again -- |
It seams that if you have: loop x = Free.pure (x + 1) >>= loop
oops = loop 0
|
That will always happen. You are building an infinite tree strictly since there’s no eta expansion within loop. |
Ok, but here it's eta expanded version: loop x = Free.pure (x + 1) >>= \k => loop k
oops = loop 0 Because bind is implemented like this: |
A couple of updates for y'all:
At this point, beyond any discussion we need to have about potential issues with the implementation itself, it really comes down to whether these API changes are palatable or not. If they aren't, then I'd like to see where we can meet in the middle on some of these function renames and changes. |
Ah, yep! That's a great point. It needs to just be suspended as a bind instead of taking a shortcut. |
I have reverted changes that would break the existing API. I'm still of the opinion we should make those changes, but I shouldn't have pushed to make them in this PR. I've updated the PR description with details: This PR now offers a performance improvement without breaking changes. |
@natefaubion @safareli I've verified that this blows up (at compile time): loop x = Free.pure (x + 1) >>= \k => loop k
oops = loop 0 I'm not quite sure how to suspend Here's the existing implementation (source): instance bindFree :: Bind (Free f) where
bind (Pure a) k = k a
bind (Bind a bs) k = Bind a (Node (unsafeCoerce bs) (Leaf (unsafeCoerce k))) I can partially rewrite this as: instance bindFree :: Bind (Free f) where
bind (Pure a) k = Bind (?wat a) (Leaf (unsafeCoerce k))
bind (Bind a bs) k = Bind a (Node (unsafeCoerce bs) (Leaf (unsafeCoerce k))) producing the error
I'm open to suggestions on how to address this, and I'll keep working on it on my own! |
This shouldwork but it might not be as elegant = Pure a
+ | BindPure UnsafeBoundValue (FreeBinds f UnsafeBoundValue a)
| Bind (f UnsafeBoundValue) (FreeBinds f UnsafeBoundValue a) |
@safareli That's a good idea. I gave it a try and I was able to get it working except for one place -- resume'
:: forall f a r
. (forall b. f b -> (b -> Free f a) -> r)
-> (a -> r)
-> Free f a
-> r
resume' bind' pure' = case _ of
Pure a -> pure' a
BindPure a bs -> bind' ?a (go1 bs)
Bind a bs -> bind' a (go1 bs)
where
go1 :: forall x y. FreeBinds f x y -> x -> Free f y
go1 bs x = case bs of
Leaf k -> k x
Node l r -> case uncons l r of
FreeCons k bs' -> case k x of
Pure a -> go1 bs' a
BindPure a bs'' -> BindPure a (Node bs'' bs')
Bind a bs'' -> Bind a (Node bs'' bs')
Hoist nat bs' ->
go2 nat bs' x
go2 :: forall g x y. (UnsafeBoundF ~> g) -> FreeBinds UnsafeBoundF x y -> x -> Free g y
go2 nat bs x = case bs of
Leaf k -> hoistFree nat (k x)
Node l r -> case uncons l r of
FreeCons k bs' -> case k x of
Pure a -> go2 nat bs' a
BindPure a bs'' -> BindPure a (Hoist nat (Node bs'' bs'))
Bind a bs'' -> Bind (nat a) (Hoist nat (Node bs'' bs'))
Hoist nat' bs' ->
go2 (nat <<< nat') bs' x That's quite a bit of code -- this is the excerpt worth looking at: resume' bind' pure' = case _ of
Pure a -> pure' a
BindPure a bs -> bind' ?a (go1 bs)
Bind a bs -> bind' a (go1 bs)
where
... which produces the following error with the typed hole:
...which feels like the same issue, deeper in the code. This possibly means that |
I tested a new implementation for Free when benchmarking Halogen Hooks and found it sped up the library (and Halogen) by as much as 25% while using less memory.
This PR adds this new implementation while preserving the existing API. The benchmarks prove the new implementation is significantly more performant than the existing v5.2.0 implementation -- an even greater performance difference than between the current version and v0.6.1 (see benchmarks below).
That means Halogen, Run, and other Free-based applications will see a nice performance boost going forward.
API Changes
This PR preserves the existing API and introduces three new functions:
run
,interpret
, andinterpretRec
.Of these functions,
interpret
is a drop-in replacement forsubstFree
and forfoldFree
, andinterpretRec
is a drop-in replacement forfoldFree
(with theMonadRec
constraint).I think these names better indicate what the functions are for (interpreting a Free structure into another monad or Free structure, or doing the same thing except with a
MonadRec
constraint).substFree
andfoldFree
still exist, just with a deprecation warning. If desired, I can remove the deprecation warnings and only add theinterpret
function.In the future I think we should adopt more of @natefaubion's implementation, which I can put in a separate issue. In my opinion it provides an API with simpler, more approachable naming conventions than those which currently exist. Specifically, it drops suffixes except for functions which have a
MonadRec
constraint, so:liftF
->lift
suspendF
->suspend
hoistFree
->hoist
runFree
->run
runFreeM
->runRec
In addition, the
substFree
andfoldFree
functions are renamed to better clarify what they're doing:substFree
->interpret
foldFree
->interpretRec
However, I think these changes are worth exploring in a separate proposal because they would be breaking changes.
Notes
Only commits from 3bf25fa onward are relevant to this PR. The prior commits are the same as Update benchmarks #103, which just fixes the benchmarks but doesn't change any of the implementations. You might want to look at the diff with only that PR in mind.
To avoid too many breaking changes I've added aI have instead preserved the existing API.Compat
module for any modules which would no longer work in user code. TheseCompat
modules can be directly dropped in to preserve existing behavior in libraries and applications. But I think they should be removed in a future version.Benchmark Results
(Note: v6.0.1 stack overflows on this benchmark, so we're only comparing this implementation to the current v5.2.0 implementation)
