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

lawful concurrent version #31

Merged
merged 23 commits into from
Jan 21, 2017
Merged

lawful concurrent version #31

merged 23 commits into from
Jan 21, 2017

Conversation

safareli
Copy link
Owner

@safareli safareli commented Nov 6, 2016

This is breaking change.

For changed in API, see test/concurrency.js

Old implementation was relaying that the target monad was not lawful.
initially i have ported old version of haskell-free-concurrent by @srijs, because I did not quite understood the lenses operators used in the latest version and the latest version was a bit hard to understand. But recently I did another try to understand it and here is the result.

What has changed

Now we have Concurrent structure which is a Monad, it holds Parallel or Sequential computations which are itself holding Concurrent computations.

-- Applicative
data Par f a where
  Pure :: a -> Par f a
  Apply :: f a -> Par f (a -> b) -> Par f b

-- Monad
data Seq f a where
  Pure :: a -> Seq f a
  Roll :: f a -> (a -> Seq f b) -> Seq f b

-- Monad
data Concurrent f a where
  Lift :: f a -> Concurrent f a
  Seq :: Seq (Concurrent f) a -> Concurrent f a
  Par :: Par (Concurrent f) a -> Concurrent f a

When operating on Concurrent structures it's behaving as Sequential. but in cases you want Parallel behaviour you can call .par() on it and it will return Par object which is only Applicative. then you can move back to Concurrent using Concurrent.Par.

Concurrent.prototype.par :: Concurrent f a ~> Par (Concurrent f) a
Concurrent.prototype.seq :: Concurrent f a ~> Seq (Concurrent f) a

here is visual version:
Graph

Interpreter

Currently Interpreter looks like this:

data Interpreter f g m = Interpreter
  { runSeq :: forall x. f x -> m x
  , runPar :: forall x. f x -> g x
  , seqToPar :: forall x. m x -> g x
  , parToSeq :: forall x. g x -> m x
  , Seq :: TypeRep m
  , Par :: TypeRep g
  }

Concurrent.prototype.interpret :: (Monad m, ChainRec m, Applicative g) => 
  Concurrent f a ~>
  Interpreter f g m ->
  m a

TODO

  • refactor tests.
  • refactor interpret (use folds)
  • make Concurrent and Seq ChainRec
  • think on graft, hoist retract
  • remove daggy
  • update readme, package.json and build stuff
  • test if deep Par structures overflow stack on fold and if yes optimise it using tail recursion.
  • make Seq implementation detail so that user will only deal with Concurrent and Par as Concurrent is delegating actions to underlying Seq

Related to #27

@safareli
Copy link
Owner Author

safareli commented Nov 6, 2016

@jdegoes @kwijibo what you think

@jdegoes
Copy link

jdegoes commented Nov 7, 2016

@safareli How would I optimize some parallel fragment?

@safareli
Copy link
Owner Author

safareli commented Nov 7, 2016

@jdegoes Can you give some example of optimising parallel fragment in SeqPar and I would try to equivalent here.

@jdegoes
Copy link

jdegoes commented Nov 7, 2016

It's a transformation from FreeAp f to FreeAp g in the general case, or a transformation from FreeAP f to g in a more specific case. One need's the FreeAp's analyze or something similar.

@safareli
Copy link
Owner Author

safareli commented Nov 7, 2016

Yah I get it, but what I asked was actual useful example of optimising parallel fragmen in SeqPar so i have something to test against.

@codecov-io
Copy link

codecov-io commented Nov 8, 2016

Current coverage is 98.82% (diff: 98.80%)

Merging #31 into dev will decrease coverage by 1.17%

@@           dev        #31   diff @@
=====================================
  Files        2          7     +5   
  Lines       95        170    +75   
  Methods      0          0          
  Messages     0          0          
  Branches     0          0          
=====================================
+ Hits        95        168    +73   
- Misses       0          2     +2   
  Partials     0          0          

Powered by Codecov. Last update 50fa3bc...f1284d7

@jdegoes
Copy link

jdegoes commented Nov 8, 2016

For example, compiling an applicative parser & achieving fusion.

@safareli
Copy link
Owner Author

safareli commented Nov 16, 2016

@jdegoes If you have some parallel fragment of type Par i a it's same as FreeAp i a so you can optimise it as you like, but when you transform it to Concurrent i a then it might be a bit tricky.

for example:

> a = lift2(a => b =>  `${a}-{b}`, Par.lift(1), Par.lift(2))
Par.Apply(1, Par.Apply(2, Par.Pure(a => bc(ab(a)))))
a :: Par Number String

To be able to use it alongside with Seq we need to hoist it using Concurrent.lift:

> b = a.hoistPar(Concurrent.lift)
Par.Apply(Concurrent.Lift(2), Par.Apply(Concurrent.Lift(1), Par.Pure(a => bc(ab(a)))))
b :: Par (Concurrent Number) String

now it's valid argument to Concurrent.Par :: Par (Concurrent f) a -> Concurrent f a:

> c = Concurrent.Par(b)
Concurrent.Par(Par.Apply(Concurrent.Lift(2), Par.Apply(Concurrent.Lift(1), Par.Pure(a => bc(ab(a))))))
c :: Concurrent Number String

For now i don't have some special way to optimise Parallel fragments but you can see that it's possible for c. The tricky part starts when we also have Seq in Concurrent structure.

> d = c.chain(v => Concurrent.lift(3).map(x => `${v}: ${x}`))
// we could also use `lift2` as `ap` is derived from `chain`
Concurrent.Seq(
  Seq.Roll(
    Concurrent.Par(
      Par.Apply(Concurrent.Lift(2), Par.Apply(Concurrent.Lift(1), Par.Pure(a => bc(ab(a)))))
    ),
    a => chain(bc, ab(a))
  )
)
d :: Concurrent Number String

... and if you use d like this:

> e = Concurrent.Par(lift2(a => b => `${a}-{b}`, d.par(), Concurrent.lift(4).par()))
Concurrent.Par(
  Par.Apply(// [3]
    Concurrent.Seq(
      Seq.Roll(
        Concurrent.Par(
          Par.Apply(Concurrent.Lift(2), Par.Apply(Concurrent.Lift(1), Par.Pure(a => bc(ab(a))))) // [1]
        ), 
        a => chain(bc, ab(a))
      )
    ),
    Par.Apply(Concurrent.Lift(4), Par.Pure(a => bc(ab(a)))) // [2]
  )
)
e :: Concurrent Number String

then you can do any optimisation on parts like [1] and [2] where we have "pure" instructions like Concurrent.Lift(...) in Par fragment, but it would be tricky to do same with Par fragments which contain Concurrent.Seq(Seq(...)) like [3]

Main advantage of Concurrent over your SecPar is that if you have fragments of type Concurrent i a you can still use them with Parallel fragments.

a ::  Concurrent Number String // Might contains Par and Seq
b ::  Concurrent Number String // Might contains Par and Seq
Concurrent.Par(lift2(z => x => x + z, a.par(), b.par())) :: Concurrent Number String

@safareli
Copy link
Owner Author

safareli commented Dec 2, 2016

it's almost done:

  • one thing left is that complexity of Par (Free Applicative) is quadratic and is could not be usefull to traverse large array for example but it's fixable using this aproach https://www.eyrie.org/~zednenem/2013/05/27/freeapp which is adopted in purescript-freeap
  • second is we can make Seq implementation detail, user could only deal with Concurrent and Par as Concurrent is delegating actions to underlying Seq, first solution would be export static types (like in static-land) for Sequential and Parallel computations, which would use Concurrent/Par/Seq internally, but I would work on this in another PR.

@safareli
Copy link
Owner Author

🎉 💃

As of last commit Par (FreeApplicative) has O(1) complexity on map/ap and O(n) on fold, also fold is stack safe.

Just a bit of refactoring is left for it to be merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants