typelevel/cats

Make FreeApplicative stack safe

Closed this issue · 8 comments

Make FreeApplicative stack safe

I would be interested as well, @adelbertc are there any pointers how to do this?

freeAp might have stack size issue when traversing but the issue might be in traverse method of Array which will be solved in this PR: sanctuary-js/sanctuary-type-classes#19

but you might have issue with quadratic complexity of free applicative on which here are some related issues:

and helpful links:

Recently I have Implemented FreeApplicative (Par in safareli/free#31) which has O(1) complexity on map/ap and O(n) on fold. Also fold is stacksafe, which was not possible with other optimised implementations out there, as they use functions to build up a structure which requires extra stack on fold, leading to stack overflow. here the Free Applicative structure itself is not using any function, and the fold is written so, that function's stack is not used. I think same technique could be applied to other "lazyish" structures to fix stack issue without need for TCO support in environment.

btw same issue is in scalaz/scalaz#1301 too

@safareli Can you explain how your Par implementation works, specifically foldPar? The JS is incomprehensible to me. The ADT changes are interesting, because it's the opposite of the direction I've typically gone using RWR, i.e., an explicit Lift case and function from FA[F, A] => FA[F, A => B] => FA[F, B] rather than F[A] => FA[F, A => B] => FA[F, B].

@edmundnoble can you clarify this part:

RWR, i.e., an explicit Lift case and function from FA[F, A] => FA[F, A => B] => FA[F, B] rather than F[A] => FA[F, A => B] => FA[F, B].

Eff's implementation of Free, for example, has something akin to case class Impure[R, X, A](union: Union[R, X], continuation: X => Eff[R, A]) but with RWR on the continuation argument. See https://github.com/atnos-org/eff/blob/master/shared/src/main/scala/org/atnos/eff/Eff.scala#L85. It's not too relevant though; I'm really just wondering how foldPar works.

how foldPar works.

Hope this makes sense if not let me know:

// data Par f a where
//  Pure :: {x :: a} -> Par f a
//  Lift :: {i :: f a} -> Par f a
//  Ap :: {f :: Par f (a -> b), x :: Par f a} -> Par f b
// TypeRep f = { of :: forall a. a -> f a}
// foldPar :: (Applicative g) => Par f a ~> (Ɐ x. f x -> g x, TypeRep g) -> g a
foldPar(f, T) {
  // `argsF` contains Par structures which will eventually be applied to some function
  const argsF = [this /*:: Par f a */]
  // `fns` contains Par structures which resolve to some function
  const fns = []
  // we have a loop which runs until we have something to return 
  while (true) {
    // we pop last Par object from `argsF` 
    // we have guaranty that argsF will not be empty here
    let argF = argsF.pop()
    // if the `argF` is `Ap` node
    if (Ap.is(argF)) {
      // we get length of argsF {{{{{{{TK add why}}}}}}}
      const lengthInitial = argsF.length
      // until `argF` is `Ap`
      while (Ap.is(argF)) {
        // so Ap node of type `Par f b` has structure like this:
        //    {f :: Par f (a -> b), x :: Par f a}
        // so here we try to go dig into `f` part to be able to obtain 
        // function `a -> b` and in this process we push the `x :: Par f a`
        // to `argsF` so if initially `` looked like :
        //    argsF = [Ap(Ap(Pure(x => y => x + y), Pure(1)), Pure(2))]
        //    fns = []
        // after this loop ends we would have
        //    argsF = [Pure(2), Pure(1)]
        //    argF = Pure(x => y => x + y)
        argsF.push(argF.x)
        argF = argF.f
      }
      // now as we went as deep as possible in `f` branch of `Ap` node `argF`
      // is either `Pure` node with function or `Lift` with some instruction
      // in both cases we now need to interpret it i.e create pure value in target
      // structure using `of(val, T)` or cal `f` with instruction which will return
      // interpret the instruction into target structure. in both cases they will 
      // eventually produce some function for which we have arguments in `argsF`
      // so `foldArg(argF, f, T)` transforms  `Free f (a -> b)` into `g (a -> b)`
      // and then we create the internal object which contains the `g (a -> b)` and
      // number of arguments it needs to take by diffing initial length of argsF 
      // (which was 0 for the example case 2 so length will be 2) with current length
      // if `f = x => new Identity(x)) and `T.of = f` then 
      // fns at this point  will be `[Identity(x => y => x + y)]`
      fns.push(Fn(foldArg(argF, f, T), argsF.length - lengthInitial))
      // at this point we have "consumed" top `Ap` node from `argsF` and we `continue`
      // if there will be `Ap` nodes in the `argsF` this part will try to simplify
      // again and again
      continue
    }
    // at this point argT is not Ap node so it's either Pure or Lift which we can
    // interpret into g as we did in `Ap` case
    const argT = foldArg(argF, f, T)
    // if `fns` is empty this means we are done and we return argT
    if (fns.length === 0) {
      return argT
    }
    // at this point we must have a function in fns which we pop
    let fn = fns.pop()
    // and we apply `argT :: g a` to `fn :: g (a -> b)` and we will get `g b`
    let res = ap(fn.f, argT)

    // we check how meny times we needed to apply to the `fn`
    // if it's more then 1 then the `res` contains function (curried) which 
    // wants to take more arguments which would be in the `argsF`
    if (fn.length > 1) {
      // so we need to push  the `res` to the `fns` but with smaller `length`
      // and continue everything
      fns.push(Fn(res, fn.length - 1))
      continue
    }
    // at this point the `fn` has no more arguments so we check if there are other
    // Par objects which evaluate to some function in `fns` and if we have one
    while (fns.length > 0) {
      // we pop it from the `fns`
      fn = fns.pop()
      // and apply `res` to it
      res = ap(fn.f, res)
      // here we check if result will need more arguments
      if (fn.length > 1) {
        // in which case we push it to the `fns` with smaller `length`
        fns.push(Fn(res, fn.length - 1))
        break
      }
    }
    // if we reach this position then and `fns` is empty
    if (fns.length === 0) {
      // then we are done 🎉
      return res
    }
  }
}



// Internal helper function for foldPar it folds only Pure and Lift nodes
const foldArg = (node, f, T) => {
  // check if `node` is `Pure` node and if so
  if (Pure.is(node)) {
    // take it's pure value and put it in T
    // i.e. call `pure`/`return` of the target structure
    return of(T, node.x)
  // if `node` is `Lift` node then
  } else if (Lift.is(node)) {
    // we call `f` with instruction contained in the `Lift` node
    // with interprets the instruction into target structure and we return it
    return f(node.i)
  }
}

// Internal helper structure for foldPar it contains an Applicative containing
// a function and information on how many argument it needs
// type Fn g a b = { fun:: g (a -> b), length:: Number}
const Fn = (f, length) => ({ f, length })