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

Initial sketch of a free applicative implementation #9

Closed
wants to merge 2 commits into from

Conversation

ethul
Copy link
Owner

@ethul ethul commented Nov 30, 2016

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
Copy link
Owner Author

ethul commented Dec 2, 2016

foldFreeAp

foldfreeap-71acf67-9ccd7db

retractFreeAp

retractfreeap-71acf67-9ccd7db

@paf31
Copy link

paf31 commented Dec 2, 2016

Wow 😄

@ethul
Copy link
Owner Author

ethul commented Dec 2, 2016

:) 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
Copy link
Owner Author

ethul commented Dec 2, 2016

(Updated to improve variance)

foldFreeAp (small)

foldfreeap-71acf67-9ccd7db-small

retractFreeAp (small)

retractfreeap-71acf67-9ccd7db-small

@paf31
Copy link

paf31 commented Dec 2, 2016

@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
Copy link

paf31 commented Dec 2, 2016

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
Copy link
Owner Author

ethul commented Dec 2, 2016

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
Copy link

paf31 commented Dec 2, 2016

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
Copy link
Owner Author

ethul commented Dec 2, 2016

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
Copy link
Contributor

safareli commented Dec 2, 2016

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

@garyb
Copy link
Contributor

garyb commented Dec 2, 2016

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
Copy link
Contributor

safareli commented Dec 2, 2016

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

@ethul
Copy link
Owner Author

ethul commented Dec 4, 2016

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
Copy link
Contributor

safareli commented Dec 9, 2016

@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
Copy link
Owner Author

ethul commented Dec 9, 2016

@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
Copy link
Contributor

safareli commented Dec 9, 2016

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
Copy link
Owner Author

ethul commented Dec 9, 2016

@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
Copy link

paf31 commented Dec 9, 2016

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
Copy link
Owner Author

ethul commented Dec 9, 2016 via email

@safareli
Copy link
Contributor

btw just wrote about the stack safe function composition

@ethul
Copy link
Owner Author

ethul commented Dec 10, 2016 via email

@safareli
Copy link
Contributor

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

@ethul
Copy link
Owner Author

ethul commented Dec 17, 2016 via email

@safareli
Copy link
Contributor

safareli commented Jan 15, 2017

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
Copy link
Owner Author

ethul commented Jan 15, 2017 via email

@safareli
Copy link
Contributor

safareli commented Jan 18, 2017

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
Copy link
Owner Author

ethul commented Jan 27, 2018

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