Skip to content

Commit 60db495

Browse files
committed
Merge pull request #41 from ethul/topic/benchotron
Topic/benchotron
2 parents 7b15669 + 98cbac3 commit 60db495

27 files changed

+443
-103
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.DS_Store
12
/.*
23
!/.gitignore
34
!/.travis.yml

README.md

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,43 @@
44
[![Build Status](https://travis-ci.org/purescript/purescript-free.svg?branch=master)](https://travis-ci.org/purescript/purescript-free)
55
[![Dependency Status](https://www.versioneye.com/user/projects/55848c7336386100150003e9/badge.svg?style=flat)](https://www.versioneye.com/user/projects/55848c7336386100150003e9)
66

7-
Free monads, Cofree comonads, Yoneda and Coyoneda functors, and the Trampoline monad.
7+
Free monad, Cofree comonad, Yoneda and Coyoneda functors, and the Trampoline monad implementations for PureScript.
8+
9+
The Free monad implementation is represented using a sequential data structure.
10+
11+
See the following reference for further information.
12+
* [Relection without Remorse](http://okmij.org/ftp/Haskell/zseq.pdf) (Ploeg and Kiselyov 2014)
813

914
## Installation
1015

1116
```
1217
bower install purescript-free
1318
```
1419

15-
## Module documentation
20+
## Documentation
21+
22+
* [Control.Monad.Free](docs/Control/Monad/Free.md)
23+
* [Control.Comonad.Cofree](docs/Control/Comonad/Cofree.md)
24+
* [Data.Yoneda](docs/Data/Yoneda.md)
25+
* [Data.Coyoneda](docs/Data/Coyoneda.md)
26+
* [Control.Monad.Trampoline](docs/Control/Monad/Trampoline.md)
27+
28+
## Benchmarks
1629

17-
### Free/Cofree
30+
The following benchmarks compare the current implementation with the implementation at `v0.6.1` (purescript/purescript-free@0df59c5d459fed983131856886fc3a4b43234f1f), which used the `Gosub` technique to defer monadic binds.
1831

19-
- [Control.Monad.Free](docs/Control/Monad/Free.md)
20-
- [Control.Comonad.Cofree](docs/Control/Comonad/Cofree.md)
32+
The benchmarks may be run as follows. Note that `pulp` must be on your path.
33+
34+
```bash
35+
npm install
36+
37+
npm run-script benchmark
38+
```
2139

22-
### Yoneda/Coyoneda
40+
![left-bind-small](benchmark/left-bind-small.png)
2341

24-
- [Data.Yoneda](docs/Data/Yoneda.md)
25-
- [Data.Coyoneda](docs/Data/Coyoneda.md)
42+
![left-bind-large](benchmark/left-bind-large.png)
2643

27-
### Trampoline monad
44+
![right-bind-small](benchmark/right-bind-small.png)
2845

29-
- [Control.Monad.Trampoline](docs/Control/Monad/Trampoline.md)
46+
![right-bind-large](benchmark/right-bind-large.png)

benchmark/Benchmark/Free0df59c5.purs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
module Benchmark.Free0df59c5
2+
( Free(..), GosubF()
3+
, FreeC(..)
4+
, MonadFree, wrap
5+
, Natural()
6+
, liftF, liftFI, liftFC, liftFCI
7+
, pureF, pureFC
8+
, mapF, mapFC
9+
, bindF, bindFC
10+
, injF, injFC
11+
, runFree
12+
, runFreeM
13+
, runFreeC
14+
, runFreeCM
15+
) where
16+
17+
import Prelude
18+
19+
import Data.Exists
20+
21+
import Control.Monad.Trans
22+
import Control.Monad.Eff
23+
import Control.Monad.Rec.Class
24+
25+
import Data.Identity
26+
import Data.Coyoneda
27+
import Data.Either
28+
import Data.Function
29+
import Data.Maybe
30+
import Data.Inject (Inject, inj)
31+
32+
type Natural f g = forall a. f a -> g a
33+
34+
newtype GosubF f a i = GosubF { a :: Unit -> Free f i, f :: i -> Free f a }
35+
36+
gosub :: forall f a i. (Unit -> Free f i) -> (i -> Free f a) -> Free f a
37+
gosub a f = Gosub $ mkExists $ GosubF { a: a, f: f}
38+
39+
-- | The free `Monad` for a `Functor`.
40+
-- |
41+
-- | The implementation defers the evaluation of monadic binds so that it
42+
-- | is safe to use monadic tail recursion, for example.
43+
data Free f a = Pure a
44+
| Free (f (Free f a))
45+
| Gosub (Exists (GosubF f a))
46+
47+
-- | The free `Monad` for an arbitrary type constructor.
48+
type FreeC f = Free (Coyoneda f)
49+
50+
-- | The `MonadFree` class provides the `wrap` function, which lifts
51+
-- | actions described by a generating functor into a monad.
52+
-- |
53+
-- | The canonical instance of `MonadFree f` is `Free f`.
54+
class MonadFree f m where
55+
wrap :: forall a. f (m a) -> m a
56+
57+
instance functorFree :: (Functor f) => Functor (Free f) where
58+
map f (Pure a) = Pure (f a)
59+
map f g = liftA1 f g
60+
61+
instance applyFree :: (Functor f) => Apply (Free f) where
62+
apply = ap
63+
64+
instance applicativeFree :: (Functor f) => Applicative (Free f) where
65+
pure = Pure
66+
67+
instance bindFree :: (Functor f) => Bind (Free f) where
68+
bind (Gosub g) k = runExists (\(GosubF v) -> gosub v.a (\x -> gosub (\unit -> v.f x) k)) g
69+
bind a k = gosub (\unit -> a) k
70+
71+
instance monadFree :: (Functor f) => Monad (Free f)
72+
73+
instance monadTransFree :: MonadTrans Free where
74+
lift f = Free $ do
75+
a <- f
76+
return (Pure a)
77+
78+
instance monadFreeFree :: (Functor f) => MonadFree f (Free f) where
79+
wrap = Free
80+
81+
instance monadRecFree :: (Functor f) => MonadRec (Free f) where
82+
tailRecM f u = f u >>= \o -> case o of
83+
Left a -> tailRecM f a
84+
Right b -> pure b
85+
86+
-- | Lift an action described by the generating functor `f` into the monad `m`
87+
-- | (usually `Free f`).
88+
liftF :: forall f m a. (Functor f, Monad m, MonadFree f m) => f a -> m a
89+
liftF = wrap <<< map pure
90+
91+
-- | Lift an action described by the generating type constructor `f` into
92+
-- | `Free g` using `Inject` to go from `f` to `g`.
93+
liftFI :: forall f g a. (Inject f g, Functor g) => f a -> Free g a
94+
liftFI fa = liftF (inj fa :: g a)
95+
96+
-- | Lift an action described by the generating type constructor `f` into the monad
97+
-- | `FreeC f`.
98+
liftFC :: forall f a. f a -> FreeC f a
99+
liftFC = liftF <<< liftCoyoneda
100+
101+
-- | Lift an action described by the generating type constructor `f` into
102+
-- | `FreeC g` using `Inject` to go from `f` to `g`.
103+
liftFCI :: forall f g a. (Inject f g) => f a -> FreeC g a
104+
liftFCI fa = liftFC (inj fa :: g a)
105+
106+
-- | An implementation of `pure` for the `Free` monad.
107+
pureF :: forall f a. (Applicative f) => a -> Free f a
108+
pureF = Free <<< pure <<< Pure
109+
110+
-- | An implementation of `pure` for the `FreeC` monad.
111+
pureFC :: forall f a. (Applicative f) => a -> FreeC f a
112+
pureFC = liftFC <<< pure
113+
114+
-- | Use a natural transformation to change the generating functor of a `Free` monad.
115+
mapF :: forall f g a. (Functor f, Functor g) => Natural f g -> Free f a -> Free g a
116+
mapF t fa = either (\s -> Free <<< t $ mapF t <$> s) Pure (resume fa)
117+
118+
-- | Use a natural transformation to change the generating type constructor of
119+
-- | a `FreeC` monad to another functor.
120+
mapFC :: forall f g a. (Functor g) => Natural f g -> FreeC f a -> Free g a
121+
mapFC t = mapF (liftCoyonedaTF t)
122+
123+
-- | Use a natural transformation to interpret one `Free` monad as another.
124+
bindF :: forall f g a. (Functor f, Functor g) => Free f a -> Natural f (Free g) -> Free g a
125+
bindF fa t = either (\m -> t m >>= \fa' -> bindF fa' t) Pure (resume fa)
126+
127+
-- | Use a natural transformation to interpret a `FreeC` monad as a different
128+
-- | `Free` monad.
129+
bindFC :: forall f g a. (Functor g) => FreeC f a -> Natural f (Free g) -> Free g a
130+
bindFC fa t = bindF fa (liftCoyonedaTF t)
131+
132+
-- | Embed computations in one `Free` monad as computations in the `Free` monad for
133+
-- | a coproduct type constructor.
134+
-- |
135+
-- | This construction allows us to write computations which are polymorphic in the
136+
-- | particular `Free` monad we use, allowing us to extend the functionality of
137+
-- | our monad later.
138+
injF :: forall f g a. (Functor f, Functor g, Inject f g) => Free f a -> Free g a
139+
injF = mapF inj
140+
141+
-- | Embed computations in one `FreeC` monad as computations in the `FreeC` monad for
142+
-- | a coproduct type constructor.
143+
-- |
144+
-- | This construction allows us to write computations which are polymorphic in the
145+
-- | particular `Free` monad we use, allowing us to extend the functionality of
146+
-- | our monad later.
147+
injFC :: forall f g a. (Inject f g) => FreeC f a -> FreeC g a
148+
injFC = mapF (liftCoyonedaT inj)
149+
150+
resume :: forall f a. (Functor f) => Free f a -> Either (f (Free f a)) a
151+
resume f = case f of
152+
Pure x -> Right x
153+
Free x -> Left x
154+
g -> case resumeGosub g of
155+
Left l -> Left l
156+
Right r -> resume r
157+
where
158+
resumeGosub :: Free f a -> Either (f (Free f a)) (Free f a)
159+
resumeGosub (Gosub g) =
160+
runExists (\(GosubF v) -> case v.a unit of
161+
Pure a -> Right (v.f a)
162+
Free t -> Left ((\h -> h >>= v.f) <$> t)
163+
Gosub h -> runExists (\(GosubF w) -> Right (w.a unit >>= (\z -> w.f z >>= v.f))) h) g
164+
165+
-- | `runFree` runs a computation of type `Free f a`, using a function which unwraps a single layer of
166+
-- | the functor `f` at a time.
167+
runFree :: forall f a. (Functor f) => (f (Free f a) -> Free f a) -> Free f a -> a
168+
runFree fn = runIdentity <<< runFreeM (Identity <<< fn)
169+
170+
-- | `runFreeM` runs a compuation of type `Free f a` in any `Monad` which supports tail recursion.
171+
-- | See the `MonadRec` type class for more details.
172+
runFreeM :: forall f m a. (Functor f, MonadRec m) => (f (Free f a) -> m (Free f a)) -> Free f a -> m a
173+
runFreeM fn = tailRecM \f ->
174+
case resume f of
175+
Left fs -> Left <$> fn fs
176+
Right a -> return (Right a)
177+
178+
-- | `runFreeC` is the equivalent of `runFree` for type constructors transformed with `Coyoneda`,
179+
-- | hence we have no requirement that `f` be a `Functor`.
180+
runFreeC :: forall f a. (forall a. f a -> a) -> FreeC f a -> a
181+
runFreeC nat = runIdentity <<< runFreeCM (Identity <<< nat)
182+
183+
-- | `runFreeCM` is the equivalent of `runFreeM` for type constructors transformed with `Coyoneda`,
184+
-- | hence we have no requirement that `f` be a `Functor`.
185+
runFreeCM :: forall f m a. (MonadRec m) => Natural f m -> FreeC f a -> m a
186+
runFreeCM nat = runFreeM (liftCoyonedaTF nat)

benchmark/Benchmark/Main.purs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
module Benchmark.Main (main) where
2+
3+
import Prelude
4+
5+
import Control.Monad.Eff
6+
import Control.Monad.Free
7+
import Control.Monad.Trampoline
8+
9+
import Data.Array
10+
import Data.Foldable
11+
12+
import Benchotron.Core
13+
import Benchotron.UI.Console
14+
15+
import Test.QuickCheck.Gen (vectorOf)
16+
17+
import qualified Benchmark.Trampoline0df59c5 as T
18+
19+
leftBindSmallBenchmark :: Benchmark
20+
leftBindSmallBenchmark = mkBenchmark
21+
{ slug: "left-bind-small"
22+
, title: "Left associated binds (small - " ++ show inputsPerSize ++ " inputs per size)"
23+
, sizes: [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 250, 500, 1000]
24+
, sizeInterpretation: "Number of binds"
25+
, inputsPerSize: inputsPerSize
26+
, gen: \n -> vectorOf n (pure 0.0)
27+
, functions: [ benchFn "Free" (runTrampoline <<< binds)
28+
, benchFn "Free v0.6.1" (T.runTrampoline <<< bindsT)
29+
]
30+
}
31+
where
32+
inputsPerSize :: Int
33+
inputsPerSize = 100
34+
35+
binds :: Array Number -> Trampoline Number
36+
binds as = foldl (\b a -> b >>= const (gen a)) (gen 0.0) as
37+
38+
gen :: forall a. a -> Trampoline a
39+
gen = suspend <<< done
40+
41+
bindsT :: Array Number -> T.Trampoline Number
42+
bindsT as = foldl (\b a -> b >>= const (genT a)) (genT 0.0) as
43+
44+
genT :: forall a. a -> T.Trampoline a
45+
genT = T.suspend <<< T.done
46+
47+
rightBindSmallBenchmark :: Benchmark
48+
rightBindSmallBenchmark = mkBenchmark
49+
{ slug: "right-bind-small"
50+
, title: "Right associated binds (small - " ++ show inputsPerSize ++ " inputs per size)"
51+
, sizes: [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 250, 500, 1000]
52+
, sizeInterpretation: "Number of binds"
53+
, inputsPerSize: inputsPerSize
54+
, gen: \n -> vectorOf n (pure 0.0)
55+
, functions: [ benchFn "Free" (runTrampoline <<< binds)
56+
, benchFn "Free v0.6.1" (T.runTrampoline <<< bindsT)
57+
]
58+
}
59+
where
60+
inputsPerSize :: Int
61+
inputsPerSize = 100
62+
63+
binds :: Array Number -> Trampoline Number
64+
binds as = foldl (\b a -> gen a >>= const b) (gen 0.0) as
65+
66+
gen :: forall a. a -> Trampoline a
67+
gen = suspend <<< done
68+
69+
bindsT :: Array Number -> T.Trampoline Number
70+
bindsT as = foldl (\b a -> genT a >>= const b) (genT 0.0) as
71+
72+
genT :: forall a. a -> T.Trampoline a
73+
genT = T.suspend <<< T.done
74+
75+
leftBindLargeBenchmark :: Benchmark
76+
leftBindLargeBenchmark = mkBenchmark
77+
{ slug: "left-bind-large"
78+
, title: "Left associated binds (large - " ++ show inputsPerSize ++ " input per size)"
79+
, sizes: [1, 5, 10, 15, 20, 25, 30 ] <#> (* 100000)
80+
, sizeInterpretation: "Number of binds"
81+
, inputsPerSize: inputsPerSize
82+
, gen: \n -> vectorOf n (pure 0.0)
83+
, functions: [ benchFn "Free" (runTrampoline <<< binds)
84+
, benchFn "Free v0.6.1" (T.runTrampoline <<< bindsT)
85+
]
86+
}
87+
where
88+
inputsPerSize :: Int
89+
inputsPerSize = 1
90+
91+
binds :: Array Number -> Trampoline Number
92+
binds as = foldl (\b a -> b >>= const (gen a)) (gen 0.0) as
93+
94+
gen :: forall a. a -> Trampoline a
95+
gen = suspend <<< done
96+
97+
bindsT :: Array Number -> T.Trampoline Number
98+
bindsT as = foldl (\b a -> b >>= const (genT a)) (genT 0.0) as
99+
100+
genT :: forall a. a -> T.Trampoline a
101+
genT = T.suspend <<< T.done
102+
103+
rightBindLargeBenchmark :: Benchmark
104+
rightBindLargeBenchmark = mkBenchmark
105+
{ slug: "right-bind-large"
106+
, title: "Right associated binds (large - " ++ show inputsPerSize ++ " input per size)"
107+
, sizes: [1, 5, 10, 15, 20, 25, 30 ] <#> (* 100000)
108+
, sizeInterpretation: "Number of binds"
109+
, inputsPerSize: inputsPerSize
110+
, gen: \n -> vectorOf n (pure 0.0)
111+
, functions: [ benchFn "Free" (runTrampoline <<< binds)
112+
, benchFn "Free v0.6.1" (T.runTrampoline <<< bindsT)
113+
]
114+
}
115+
where
116+
inputsPerSize :: Int
117+
inputsPerSize = 1
118+
119+
binds :: Array Number -> Trampoline Number
120+
binds as = foldl (\b a -> gen a >>= const b) (gen 0.0) as
121+
122+
gen :: forall a. a -> Trampoline a
123+
gen = suspend <<< done
124+
125+
bindsT :: Array Number -> T.Trampoline Number
126+
bindsT as = foldl (\b a -> genT a >>= const b) (genT 0.0) as
127+
128+
genT :: forall a. a -> T.Trampoline a
129+
genT = T.suspend <<< T.done
130+
131+
main = runSuite [ leftBindSmallBenchmark
132+
, rightBindSmallBenchmark
133+
, leftBindLargeBenchmark
134+
, rightBindLargeBenchmark
135+
]

0 commit comments

Comments
 (0)