Skip to content

Initial sketch of a free applicative implementation - #9

Closed
ethul wants to merge 2 commits into
masterfrom
topic/performance
Closed

Initial sketch of a free applicative implementation#9
ethul wants to merge 2 commits into
masterfrom
topic/performance

Conversation

@ethul

@ethul ethul commented Nov 30, 2016

Copy link
Copy Markdown
Owner

The goal of this implementation is to improve performance
characteristics of the operations of the free applicative functor in
PureScript.

Reference #7

Reference #8

  • Add benchmarks
  • Extract and Comonad instances

The goal of this implementation is to improve performance
characteristics of the operations of the free applicative functor in
PureScript.

Reference #7

Reference #8
@ethul

ethul commented Dec 2, 2016

Copy link
Copy Markdown
Owner Author

foldFreeAp

foldfreeap-71acf67-9ccd7db

retractFreeAp

retractfreeap-71acf67-9ccd7db

@paf31

paf31 commented Dec 2, 2016

Copy link
Copy Markdown

Wow 😄

@ethul

ethul commented Dec 2, 2016

Copy link
Copy Markdown
Owner Author

:) I'd still like to work on #8 but might not have a chance until this weekend. It's on the list to do though!

@ethul

ethul commented Dec 2, 2016

Copy link
Copy Markdown
Owner Author

(Updated to improve variance)

foldFreeAp (small)

foldfreeap-71acf67-9ccd7db-small

retractFreeAp (small)

retractfreeap-71acf67-9ccd7db-small

@paf31

paf31 commented Dec 2, 2016

Copy link
Copy Markdown

@ethul Here you go. Sorry the names are a bit crap, I did this using typed holes for the most part. I think it's right though, it should correspond to the version which uses Day:

instance extendFreeAp :: Extend f => Extend (FreeAp f) where
  extend f x@(Pure a) = Pure (f x)
  extend f (Ap x) = Ap (runExists (\(ApF h j) ->
    mkExists (ApF h (\u -> extend (\g a1 -> f (map (_ $ a1) g)) (j u)))) x)

instance comonadFreeAp :: Comonad f => Comonad (FreeAp f) where
  extract (Pure a) = a
  extract (Ap x) = runExists (\(ApF h j) ->
    let a1 = extract (h unit)
        a2 = extract (j unit)
    in a2 a1) x

@paf31

paf31 commented Dec 2, 2016

Copy link
Copy Markdown

Oh, I did it on the wrong branch 😆

Oh well, I can try it on the new branch at some point, unless you beat me to it.

@ethul

ethul commented Dec 2, 2016

Copy link
Copy Markdown
Owner Author

Thanks for giving the instances a go! I will probably take a look this weekend, but I am open to any suggestions on the implementations you may have. Thanks!

@paf31

paf31 commented Dec 2, 2016

Copy link
Copy Markdown

The only suggestion I can give is to think in terms of FreeAp as a free monoid object, generated in terms of coproducts and Day convolution - that representation made it simpler for me to see what the Comonad instance was doing in terms of reannotating some sort of tree.

In fact, you might like to consider the representation in terms of Day for this library - I dare say it could be pretty efficient, although maybe not as efficient as what you have above, which is pretty impressive 😄

@ethul

ethul commented Dec 2, 2016

Copy link
Copy Markdown
Owner Author

Thanks for the insight. I will definitely dig more into this. I am interested by a representation in terms of Day. If I can get one together, I am curious to stack it against the two we have. Thanks again!

@safareli

safareli commented Dec 2, 2016

Copy link
Copy Markdown
Contributor

btw is there any resource to understand the "day convolution" which does not require too much category theory? (can't understand ncatlab)

@garyb

garyb commented Dec 2, 2016

Copy link
Copy Markdown
Contributor

Phil wrote a bit about them a while back: http://blog.functorial.com/posts/2016-08-08-Comonad-And-Day-Convolution.html but since they are very much a category theory based construct there's not going to be a whole lot of other material out there, I suspect!

@safareli

safareli commented Dec 2, 2016

Copy link
Copy Markdown
Contributor

@garyb I know that article, but didn't quite understood it, thanks tho!

@ethul

ethul commented Dec 4, 2016

Copy link
Copy Markdown
Owner Author

I've added an implementation based on Day in #10 as suggested by @paf31. I am kinda liking the results in #10 over #9, but I am wondering if anyone has thoughts on one implementation versus the other.

Another question is concerning thunks. @garyb you asked about this earlier. The current v2.0.0 implementation thunks the f i and FreeAp f (i -> a). In #9 and #10, I've removed the thunks. Not sure if we need this. Originally, it was to follow the implementation in scalaz's Ap. I am open to suggestions here though.

@safareli

safareli commented Dec 9, 2016

Copy link
Copy Markdown
Contributor

@ethul I have made 1 to 1 port of https://www.eyrie.org/~zednenem/2013/05/27/freeapp to js it's pritty fast, but if the tree is large (for example 4000) stack overflows (on 3000 it works fine)

const buildTree = n => {
  let res = FreeAp.of(function f(a) {
    return a == n - 1 ? a : f
  })
  for (let i = 0; i < n; i++) {
    res = FreeAp.lift(i).ap(res)
  }
  return res
}
buildTree(4000).foldPar(Identity.of, Identity).toString()

Can you try run benchmark with largar input (more than 2000) as i think it would overflow.

@ethul

ethul commented Dec 9, 2016

Copy link
Copy Markdown
Owner Author

@safareli You're right. I have tried this before and it does overflow. Certainly something I'd like to look into further. Thanks for bringing this up!

@safareli

safareli commented Dec 9, 2016

Copy link
Copy Markdown
Contributor

if we have large chain of function composition (f . g . h ...) and execute it we would get stack overflow, and in this implementation we have only functions :d, so i think issue is with function composition (f . g). I will investigate it further, but it's a bit criptic for me (so many functions are passed around).

hypothetical solution would be to have a heterogenous list of functions, which could be instance of Category and it's composition would just build up the List (or just make it a Functor and fmap will do the same, as . and fmap are same for function)

data FunList a b where
  One  :: (a -> b) -> FunList a b
  Cons :: (z -> b) -> FunList a z -> FunList a b
  
instance Functor (FunList z) where
  fmap :: (a -> b) -> FunList z a -> FunList z b
  fmap g f = Cons g f
  -- or just `fmap = Cons`

Then we could have some function to actually execute it in a stack safe way using for or something

run :: FunList a b -> a -> b

for example in js:

// list of functions 
const fs = Array(100000).fill().map((_,idx) => x => x + idx)
const compose = (f,g) => x => f(g(x))
// if we compose them and then run it 
fs.reduce(compose)(1) // we get stack overflow
// but if this is stack safe
fs.reduceRight((arg,f) => f(arg),1)

so if we have wrapper for function which during composition builds up a list then it could be executed without stack issues.


i think we could add more variants to the FunList type and make it a monad if needed

@ethul

ethul commented Dec 9, 2016

Copy link
Copy Markdown
Owner Author

@safareli I like your idea. Thanks for the example. I will have to dig into this a bit more this weekend, but I like the direction of this so far.

@paf31

paf31 commented Dec 9, 2016

Copy link
Copy Markdown

I bet you could write retractFreeAp as a tail recursive function pretty easily using the Day representation, although I haven't tried it yet.

@ethul

ethul commented Dec 9, 2016 via email

Copy link
Copy Markdown
Owner Author

@safareli

Copy link
Copy Markdown
Contributor

btw just wrote about the stack safe function composition

@ethul

ethul commented Dec 10, 2016 via email

Copy link
Copy Markdown
Owner Author

@safareli

Copy link
Copy Markdown
Contributor

continuation of that first article https://www.eyrie.org/~zednenem/2013/06/freeapp-2

@ethul

ethul commented Dec 17, 2016 via email

Copy link
Copy Markdown
Owner Author

@safareli

safareli commented Jan 15, 2017

Copy link
Copy Markdown
Contributor

Hey recently i was thinking on how to make it stack safe and and came up with this implementation of FreeAp which has O(1) complexity on map and ap and O(n) on fold which is also stack safe if target applicative is stacksafe. I think it this is also simpler than previous versions:

//data Par f a where
//  Pure :: a -> Par f a
//  Lift :: f a -> Par f a
//  Ap :: Par f (a -> b) -> Par f a -> Par f b
const Par = union('Par', {
  Pure: ['x'],
  Lift: ['i'],
  Ap: ['i', 'x'],
})
const { Pure, Lift, Ap } = Par

Object.assign(Par, {
  // :: a -> Par f a
  of: Pure,
  // :: f a -> Par f a
  lift: Lift,
})

const foldArg = (node, f, T) => {
  if (node.tag == 'Pure') {
    return of(T, node.x)
  } else if (node.tag == 'Lift') {
    return f(node.f)
  }
  throw new TypeError('Invalid argument for foldArg')
}

Object.assign(Par.prototype, {
  // :: Par f a ~> (a -> b) -> Par f b
  map(f) {
    return ap(Par.of(f), this)
  },
  // :: Par f a ~> Par f (a -> b) -> Par f b
  ap(that) {
    return Ap(that, this)
  },
  // :: Applicative g => Par f a ~> (Ɐ x. f x -> g x, TypeRep g) -> g a
  foldPar(f, T) {
  // # this is non stack safe version:
  // return this.cata({
  //   Pure: (x) => of(T, x),
  //   Lift: (i) => f(i),
  //   Ap: (i, x) => ap(i.foldPar(f,T), x.foldPar(f,T)),
  // })
  // # this is stack safe version:
    const args = [this]
    const fns = []
    while (true) {
      let arg = args.pop()
      if(arg.tag === 'Ap') {
        while (arg.tag == 'Ap') {
          args.push(arg.x)
          arg = arg.f
        }
        fns.push(foldArg(arg, f, T))
      } else {
        const resArg = foldArg(arg, f, T)
        if (fns.length == 0) {
          return resArg
        }
        let resFn = ap(fns.pop(), resArg)
        if (args.length > 0) {
          fns.push(resFn)
        } else if (fns.length > 0) {
          fns.push(resFn)
          return fns.reduce((fn, arg) => ap(fn, arg))
        } else {
          return resFn
        }
      }
    }
  },
  // :: Par f a ~> (Ɐ x. f x -> g x) -> Par g a
  hoistPar(f) {
    return this.foldPar(compose(Par.lift)(f), Par)
  },
  // :: (Applicative f) => Par f a ~> TypeRep f -> f a
  retractPar(m) {
    return this.foldPar(id, m)
  },
  // :: Par f a ~> (Ɐ x. f x -> Par g x) -> Par g a
  graftPar(f) {
    return this.foldPar(f, Par)
  },
})

I used this snippet to test stack safety:

const apTimes = (n) => {
    const args = []
    const f = (a) => {
      args.push(a)
      if (args.length == n) {
        return args.length
      }
      return f
    }
    let res = Par.lift(f)
    for (let i = 0; i < n; i ++ ) {
      res = ap(res, Par.lift(i))
    }
    return res
  }

  const testN = MAX_STACK

  t.equals(apTimes(testN).hoistPar(id).foldPar(Identity.of, Identity).get(), testN, 'is Par stack safe')
  t.equals(apTimes(testN).foldPar(Identity.of, Identity).get(), testN, 'Works with Identity')

Will soon update the PR safareli/free#31 I was working on.

@ethul

ethul commented Jan 15, 2017 via email

Copy link
Copy Markdown
Owner Author

@safareli

safareli commented Jan 18, 2017

Copy link
Copy Markdown
Contributor

The implementation of fold, I had written in last comment, was incorrect and this one is correct (it needs a bit of refactoring tho).

@ethul

ethul commented Jan 27, 2018

Copy link
Copy Markdown
Owner Author

Superseded by #13

@ethul ethul closed this Jan 27, 2018
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.

4 participants