fantasyland / fantasy-land

Specification for interoperability of common algebraic structures in JavaScript
MIT License
10.12k stars 377 forks source link

Add ChainRec type class #151

Closed safareli closed 8 years ago

safareli commented 8 years ago

TailRec - A type class which captures stack-safe monadic tail recursion.

It would be nice to have a common specification for Tail Recursive Monads in JS community, so that libraries like Free could use the interface to be stack safe.

in PS MonadRectype class looks like this:

class Monad m <= MonadRec m where
  tailRecM :: forall a b. (a -> m (Either a b)) -> a -> m b

So we could have something like this without stack overflow:

Identity.tailRecM((n) => {
  if (n == 0) {
    return Identity(Either.Right("DONE"))
  }
  return Identity(Either.Left(n - 1))
})(20000)

one thing is that there are multiple Either implementations in js and this spec should depend on bare minimum from any Either type. from what I have found the minimum api from Either type is to have a cata method. but before that let's see an implementation of tailRecM of Identity:

const tailRec = (f) => (a) => {
  let v = f(a)
  while (!isDone(v)) {
    v = f(getValue(v))
  }
  return getValue(v)
}

const runIdentity = (i) => i.x

Identity.tailRecM = (f) => (a) => Identity(tailRec((v) => runIdentity(f(v)))(a))

So we have seen usage of it and part of it's implementation. now what we have left is implement isDone and getValue so that they are as generic as possible.

const isDone = (e) => e.cata({
  Right: () => true,
  Left: () => false,
})

const getValue = (e) => e.cata({
  Right: (v) => v,
  Left: (v) => v,
})

so as it looks any Either with cata would work so users shouldn't be tied to some particular Either implementation (as long as it has cata). To note this Either implementation would work with tailRecM.

const Either = {
  Right: (v) => ({ cata: (c) => c.Right(v) }),
  Left: (v) => ({ cata: (c) => c.Left(v) }),
}

The hardest was to implement tailRecM for Task/Future but i have made it and after we agree on some interface I would create PRs for some popular Task/Future implementations

safareli commented 8 years ago

@paf31 @garyb your suggestions will be helpful on this

scott-christopher commented 8 years ago

Regarding Either, a generic interface could be defined using a church-encoded representation:

type Either a b = forall c. (a -> c) -> (b -> c) -> c

Such that

const Left  = x => (whenLeft, whenRight) => whenLeft(x)
const Right = x => (whenLeft, whenRight) => whenRight(x)

So you're Identity example could look like:

const tailRec = (f) => (a) => {
  let state = { done: false, value: a }
  while (!state.done) {
    f(state.value)(
      x => { state.value = x },
      x => { state.value = x, state.done = true }
    )
  }
  return state.value
}

/* other bits remain the same */

Identity.tailRecM((n) => {
  if (n == 0) {
    return Identity((_, done) => done("DONE"))
  }
  return Identity((next, _) => next(n - 1))
})(20000)
safareli commented 8 years ago

The church notation is basically a fold method of Either. I think it would be great if one could use any of Either implementations (data.either, fantasy-eithers....). For that tailRecM should depend on minimal api so it's as easy to use as possible. With the church notation user should write their own implementation of Either (those two functions Left/Right). I have show sample implementation of Either to demonstrate that tailRecM just needs cata (or possiblyfold)

SimonRichardson commented 8 years ago

I would love to see the usage of fold, it's more generalised imo.

Also 👍 on this.

rpominov commented 8 years ago

What if instead of relying on an unified Either interface the type itself would provide methods that would create Type(Left(a)) and Type(Right(a)) values?

So usage would look something like this:

Identity.tailRecM((n) => {
  if (n == 0) {
    return Identity.tailRecurDone("DONE")
  }
  return Identity.tailRecurNext(n - 1)
})(20000)

tailRecurDone and tailRecurNext names is just first what came to my mind, we could came up with better ones.

One reason why this might be a better approach is because many Future/Task implementations have built-in Either.

safareli commented 8 years ago

With fold, isDone and getValue would change to:

const isDone   = (e) => e.fold(() => false, () => true)
const getValue = (e) => e.fold((v) => v, (v) => v)
SimonRichardson commented 8 years ago

Folds all the way down.

safareli commented 8 years ago

@rpominov that's also interesting. user of some Type.tailRecM might not even use Either (most likely they do use thou). If every type conforming TailRec interface have there own two line Either implementations that would also do a trick.

something like this?:

Identity.tailRecM((n) => {
  if (n == 0) {
    return Identity.tailRecM.done("DONE")
  }
  return Identity.tailRecM.next(n - 1)
})(20000)

Also, the advantage is that, user doesn't need to care if Left does recursion of Right when done and next explicitly state what they do.

SimonRichardson commented 8 years ago

This is very reminiscent of a trampoline and has worked well there already, so I'm not against that idea either.

safareli commented 8 years ago

In the sense of accessibility, using done/next is better for newcomers, and even ones who understand Either, might get confused which one to use for looping/returning.

So now question is, which one should be used:

a b
Identity.tailRecMNext Identity.tailRecM.next
Identity.tailRecMDone Identity.tailRecM.done
SimonRichardson commented 8 years ago

b imo.

scott-christopher commented 8 years ago

In the sense of accessibility, using done/next is better for newcomers, and even ones who understand Either, might get confused which one to use for looping/returning.

Another option would be to provide the Left & Right constructors as arguments to the function given to tailRecM.

tailRecM :: MonadRec m => (a -> (b -> Either b c) -> (c -> Either b c) -> m (Either a a)) -> a -> m a

Which would look like the following to a user:

Identity.tailRecM((n, next, done) => n == 0 ? done("DONE") : next(n - 1))(20000)

This has the advantage of avoiding naming things all together, though at the expense of being a little more difficult to describe in the spec.

rjmk commented 8 years ago

If we want to avoid the 2-line Either duplication, I think it would also be possible to change the signature like this

tailRecM
  :: MonadRec m => ((a1 -> c1) -> (b1 -> c1) -> Either a1 b1 -> c1)
  -> (a -> m (Either a b)) -> a -> m b

-- Can we replace `Either` with a variable `t` here?

i.e. provide the fold to tailRecM.

rpominov commented 8 years ago

Actually next/done functions is not the same as to use Either. They're less powerful.

With Either we have more freedom of how to create return values of recursive function. We can do Type.of(Left(2)), but also we can do Type.customMethod().map(Left). While with next/done we are basically stuck with only Type.of(Left(2)) and Type.of(Right(2)).

For example in case of Future we could do something like this:

Future.tailRecM(x => {
  return someCondition(x) ? makeNetworkRequest().map(Left) : Future.of(Right('done'))
})

Edit: Although we can do this with next/done:

Future.tailRecM((x, next, done) => {
  return someCondition(x) ? makeNetworkRequest().chain(next) : done('done')
})

So just ignore the point I've made, sorry :)

rjmk commented 8 years ago

Would there be any issue with providing the internal Left and Right as arguments to tailRecM, though? So you would have something like

Future.tailRecM((x, next, done) => 
  someCondition(x) ? makeNetworkRequest.map(next) : Future.of(done('done')))
rpominov commented 8 years ago

@rjmk That also would work.


Edit: Although we wouldn't be able to use Future.of and Future.rejected as next and done.

safareli commented 8 years ago

One thing with getting next/done as arguments is that you need to pass around too many stuff for example here I'm doing .map(Either.Right) which could be .map(m.tailRecM.done) and the method needs just Type(m in that case) to get tailRecM.done/next, with having them in a dictionary we don't need to worry about order of arguments (which one is next/done?).

scott-christopher commented 8 years ago

Actually next/done functions is not the same as to use Either. They're less powerful. With Either we have more freedom of how to create return values of recursive function.

We end up with the same type when using the church-encoding or fold over Either.

fold :: (a -> c) -> (b -> c) -> Either a b -> c

If we're passing the next/done functions as arguments then there's no need to even mention Either in the type signature. It simply leaves it up to the implementation to decide how it's going to be handled.

For example, the following signature says that the only way to construct the m c is to make use of the provided a -> c and b -> c functions.

tailRecM :: MonadRec m => (a -> (a -> c) -> (b -> c) -> m c) -> a -> m b

An implementation is then free to specialise to use Either:

tailRecM :: MonadRec m => (a -> (a -> Either a b) -> (b -> Either a b) -> m (Either a b)) -> a -> m b
rpominov commented 8 years ago

We end up with the same type when using the church-encoding or fold over Either.

Yeah, I just was thinking about another signatures for next/done — a -> m c. But as I've written in "Edit", we don't have problem either way. What can be done with a -> c also can be done with a -> m c:

f :: a -> c f :: a -> m c
of(f(2)) f(2)
v.map(f) v.chain(f)

But also a -> m c is more flexible. For instance, If we use that signature, a Future type could use Future.of as next and Future.rejected as done, I think...

scott-christopher commented 8 years ago

I'm still mulling it over, but I think I agree. I also think a -> m c might make for a nicer API for end users.

My only other topic of bikeshedding here is that I'm not particularly sold on the name tailRecM. It's only a minor gripe so I'm happy to stick with it if others like it.

One suggestion for another name would be ChainRec (though this does kinda sound like Train Wreck), as I'm pretty sure it only needs a Chain and not a full Monad constraint.

class (Chain m) <= ChainRec m where
  chainRec :: (a -> (a -> m c) -> (b -> m c) -> m c) -> a -> m b
safareli commented 8 years ago

What can be done with a -> c also can be done with a -> m c

it's not quite true for example for Either we have two type constructors, or for some hypothetical type T a = F a | G a | H a we have three constructors. there is no way for next/done to decide which one to use and they should not, as it's users responsibility to create some object of type T and put in it a value which is result of calling either done or next (as user needs to create T objects it needs to have of method so it needs to be a Monad not just aChain).

See TailRec implementation for Either in PS

safareli commented 8 years ago

There are two main directions:

  1. Depend on some minimal api of Either (fold or cata)
  2. Provide functions for creating next/done values
    1. Have done/next as Type.tailRecM{Next,Done}
    2. Have done/next as Type.tailRecM.{next,done}
    3. Pass done/next as arguments to function passed to Type.tailRecM

Pluses and Minuses of 1

The spec will be something like this:

type Either a b = Left a | Right b
class Monad m <= MonadRec m where
  tailRecM :: (a -> m (Either a b)) -> a -> m b

M.tailRecM takes two values: func of type (a -> m (Either a b)) and initial argument arg of type a. the M.tailRecM will invoke func with value of type a; it will take value out, from its returned value of type m (Either a b); if it's Left a, it will call func again with the value of type a, until there is sees value Right b and in that case M.tailRecM will terminate and return m b.

To construct Right/Left values use any implementation of Either type which has method fold of type (a -> c) -> (b ->c) -> c

  • Plus:
  • Implementation does not need to reimplement Either (even though it's just two line so it's not a big plus)
  • User can use favorit implementation of Either (as long it has fold)
  • Minus:
  • User needs to know which one to use Left/Right for continuing/returning recursion
  • Some implementation of either might not have cata/fold (PR could be created or user just implement it hirself)
  • User might not actually use Either but to use tailRecM you need to import or implement something yourself

Pluses and Minuses of 2.1

The spec will be something like this:

type TailRecRes a b = Next a | Done b
class Monad m <= MonadRec m where
  tailRecM :: (a -> m (TailRecRes a b)) -> a -> m b
  tailRecMNext :: a -> TailRecRes a b
  tailRecMDone :: b -> TailRecRes a b

M.tailRecM takes two values: func of type (a -> m (TailRecRes a b)) and initial argument arg of type a. the M.tailRecM will invoke func with value of type a; it will take value out from its returned value of type m (TailRecRes a b); if it's Next a, it will call func with value of type a again, until there is value Done b and in that case M.tailRecM will terminate and return m b.

To construct Next/Done values use M.tailRecM{Next,Done} respectively.

  • Plus:
  • It's easy to explain/understand even without description
  • Type expresses what's going on
  • No need to know which one to use Left or Right vs Next or Done
  • Minus:
  • If user actually has some implementation of Either it's useless even though it has fold

Pluses and Minuses of 2.2

spec is same as 2.1 difference is just where the constructors of TailRecRes are stored, as properties of tailRecM func in this cases

Pluses and Minuses of 2.3

The spec will be something like this:

class Monad m <= MonadRec m where
  tailRecM :: (a -> (a -> c) -> (b -> c) -> m c) -> a -> m b

M.tailRecM takes two values: func of type (a -> (a -> c) -> (b -> c) -> m c) and initial argument arg of type a. the M.tailRecM will invoke func with value of type a and two functions next of type (a -> c) and done of type (b -> c); it will take value, out from its returned value of type m c; if it was created with next it will call func again with the value of type a (using which the c was created); if it was created using done then it will terminate with value of type m b.

  • Plus:
  • No word about Either or any special type
  • Minus
  • User should know order of arguments (which one is done/next?)
  • Type signature is a bit hard to understand, especially for newcomers
  • As it's taking multiple arguments it might be a bit tricky to implement functions like tailRecM{2,3}

I think we could combine #1 and #2 into somthing like this:

type Either a b = Left a | Right b
class Monad m <= MonadRec m where
  tailRecM :: (a -> m (Either a b)) -> a -> m b
  tailRecMNext :: a -> Either a b
  tailRecMDone :: b -> Either a b

M.tailRecM takes two values: func of type (a -> m (Either a b)) and initial argument arg of type a. the M.tailRecM will invoke func with value of type a; it will take value out, from its returned value of type m (Either a b); if it's Left a, it will call func again with the value of type a, until there is sees value Right b and in that case M.tailRecM will terminate and return m b.

To construct Left/Right values use M.tailRecMNext/M.tailRecMDone respectively, or any implementation of Either which has method fold of type (a -> c) -> (b ->c) -> c.

  • tailRecMNext/Done could be used to construct Either values if user does not use any Either in a project.
  • User can use favorit implementation of Either (as long it has fold)
  • Implementation does not need to reimplement Either (even though it's just two line so it's not a big plus)
  • It's easy to explain/understand even without description
  • Type expresses what's going on
  • No need to know which one to use Left or Right
scott-christopher commented 8 years ago

it's not quite true for example for Either we have two type constructors, or for some hypothetical type T a = F a | G a | H a we have three constructors. there is no way for next/done to decide which one to use and they should not

This still doesn't prohibit it as an option to use a -> m c instead of a -> c for the provided constructors. In the case of a ChainRec implementation for Either, it would be specialised to:

chainRec :: (a -> (a -> Either e c) -> (b -> Either e c) -> Either e c) -> a -> Either e b

So a user would have the choice of returning one of the following from within the provided function:

and we'd still have the option of leaving the constraint at Chain, if desired.

I'm personally not too fond of the named properties options, as the properties would have no other purpose outside of the context of the function provided to tailRecM/chainRec. Also, if we were to be strict about types, then I suspect the generic use of cata/fold would restrict the result value to be the same as the initial value (e.g. Either a a), but I could very well be mistaken here.

User should know order of arguments (which one is done/next?)

This is a valid point (likewise with the use of Either), though the type signature does indicate which is which without the need for names.

Type signature is a bit hard to understand, especially for newcomers

The same types are effectively in play whether we provide them to the function or require the end user to source them elsewhere.

As it's taking multiple arguments it might be a bit tricky to implement functions like tailRecM{2,3}

This could perhaps be tidied up by placing the next/done functions first, followed by the initial value, but I wouldn't mind with either option.

I guest my personal preference would still be leaning towards having the constructors provided to the function as arguments (whether that is a -> c or a -> m c, I don't particularly mind) as it feels like it leaks the least of the given options, leaving the choice up to the implementation without needing to expose it to end users. That said, I'd also prefer to see this proposal land rather than stall so I would happily put my preference aside to help this along.

joneshf commented 8 years ago

This discussion is amazing!

rjmk commented 8 years ago

@scott-christopher @safareli What do you think about providing a destructor to tailRecM rather than tailRecM providing the constructors?

It seems to me that allows the user to have any ADT they like, as long as they can give a way for it to be "Either-like". This is similar to depending on a fold/cata API, but without actually requiring a method to be defined (which is good as the fold for some something like T a = F a | G a | H a would presumably take 3 functions).

It would look like

tailRecM
  :: MonadRec m => ((a1 -> c1) -> (b1 -> c1) -> t a1 b1 -> c1)
  -> (a -> m (t a b)) -> a -> m b
scott-christopher commented 8 years ago

@rjmk If I understand correctly, an implementation would look something like the following?

Identity.tailRecM = (e, f, a) => {
  let state = { done: false, value: a }
  const updateState = e(
    x => ({ value: x, done: false }),
    x => ({ value: x, done: true  })
  )
  while (!state.done) {
    state = updateState(f(state.value).get())
  }
  return Identity(state.value)
}
Identity.tailRecM(Either.either,
                  n => Identity(n == 0 ? Either.Right("DONE") : Either.Left(n - 1)),
                  20).get() //=> "DONE"

I quite like the approach from the perspective of the API, but it might prove to be a bit difficult to explain in the language of the spec (I'd be happy to be proven otherwise :D)


edit: corrected as per @safarli's comment

scott-christopher commented 8 years ago

If we were to treat the types strictly with that approach we'd should also expect the type of the result would be the same as the initial value due to the type of (a1 -> c1) -> (b1 -> c1) -> t a1 b1 -> c1

I don't think that's necessary a showstopper though, as it's just a map away from modifying the resulting value to get something else for those that want to be strict with types (this is also JS ... so my example above of using Either String Number will still happily oblige).

rjmk commented 8 years ago

That's exactly what I meant!

Good point about the type strictness. I hadn't foreseen that. Just to check I understand it correctly, is it correct that we don't actually expect the type of the result to be the same as the type of the initial value, but rather the type of the first parameter to the Either-like? That is, instead of

n => n == 0 ? Either.Right(0) : Either.Left(n - 1)

we could equally well have

n => typeof n == 'string' ? Either.Right("DONE") : Either.Left("Keep going!")

and remain well-typed?

On the point about the map, I was worried momentarily about having to encode values of the desired output type in the input type, but it seems to be that one essentially needs a reliable way of doing that anyway, so the map is fine. That is, you already need something like

Identity.tailRecM
  ( Either.either
  , x => somePred(x) ? Either.Right(someF(x)) : Either.Left(someG(x))
  , someVal
  )

And then you might as well extract someF out. Am I missing any possibilities?

safareli commented 8 years ago

@scott-christopher if we have some type T a = F a | G a | H a and done/next returns values of type T c using some specific type constructor for example F, then users could not use them if they want to use other type constructors (G, H). so done/next should not return values of Type T and user should be responsible to wrap them accordingly

But even if user is responsible to create monadic values and not next/done, maybe Chain is much correct constraint for this interface, as MonadRec not necessarily needs m.of to be defined or m to be Applicative. we should discuss this too.

It would be nice to hear from @paf31 and @garyb why in PS MonadRec is called MonadRec and not BindRec

This could perhaps be tidied up by placing the next/done functions first, followed by the initial value, but I wouldn't mind with either option.

Agree if we pass next/done as arguments then they should be first and value last


About passing destructor to tailRecM, the example @scott-christopher provided is a bit incorrect:

Identity.tailRecM = (e, f, a) => {
  let state = { done: false, value: a }
  const updateState = e(
    x => ({ value: x, done: false }),
    x => ({ value: x, done: true  })
  )
  while (!state.done) {
    //`f` returns `m (t a b)` and updateState should take `t a b without `m`
    state = updateState(f(state.value))
  }
  return Identity(state.value)
}
Identity.tailRecM(Either.either,
                  // here result should be wrapped with Identity
                  n => n == 0 ? Either.Right("DONE") : Either.Left(n - 1),
                  20).get() //=> "DONE"

If we were to treat the types strictly with that approach we'd should also expect the type of the result would be the same as the initial value due to the type of (a1 -> c1) -> (b1 -> c1) -> t a1 b1 -> c1

Don't quite understand which result you mean. For example here initial value is of type Page and result is Number and return value of f is Task e (Either Page Number)

const lastPagePostLikes = Task.TailRecM(
  (page) => page.next
    ? HTTP.fetchJSON(page.next).map(Left)
    : Task.of(Right(page.posts.map(getLikes).sum()))
  , { next : 'posts/1' , posts:[]}
)

lastPagePostLikes.fork(....)

@rjmk initial value and value wrapped in Left should be of same type (as both are passed to the function).

The m.tailRecM you proposed just takes fold function so why pass every time fold function to tailRecM when it could just call fold method of value in m?


actually title of this issue is incorrect :d name of typeclass is MonadRec not TailRec. need to change it to either MonadRec or ChainRec

garyb commented 8 years ago

In PureScript it's called MonadRec as it's expected the m will satisfy the monad morphism laws (which involve return / pure) - if I remember rightly it's not obviously documented as such currently though, just in issues somewhere.

edit: Expanding on the above, we probably could have a Bind m <= BindRec m & (Monad m, BindRec m) <= MonadRec m hierarchy, with the bind and return parts of the morphism laws split between the too, it's just never come up in discussion before as I think the requirement to only satisfy Bind while providing Rec is uncommon as far as we've seen so far.

rjmk commented 8 years ago

About passing destructor to tailRecM, the example @scott-christopher provided is a bit incorrect

Oh yeah! That's easily fixed with a runIdentity though, right? Or is there a bigger problem?

initial value and value wrapped in Left should be of same type (as both are passed to the function)

I was going to say "In the case of n => typeof n == 'string' ? Either.Right('DONE') : Either.Left('Keep going!'), though, the type is 'a' or 'any'", but then realised my inspecting on type should not be allowable and would break free theorems and stuff. So thanks for the spot!

This also makes me a bit confused about the input/output type restriction.

The m.tailRecM you proposed just takes fold function so why pass every time fold function to tailRecM when it could just call fold method of value in m?

Because it might not have a fold. Or in the case of T a = F a | G a | H a its fold might not work (it would have a signature like (a -> b) -> (a -> b) -> (a -> b) -> T a -> b). Of course one could always define a wrapper that had it. Maybe my proposal is a bit too Static Land-y

safareli commented 8 years ago

Or is there a bigger problem?

yah that could be fixed easily in case of Identity. but should be fixed to be correct

method of value in m

type of f is :: a -> m (Either a b) and in m we have Either a b

and tailRecM just needs to depend on fold method to exist on Either which is wrapped with m

rjmk commented 8 years ago

yah that could be fixed easily in case of Identity. but should be fixed to be correct

👍

type of f is ::a -> m (Either a b) and in m we have Either a b

Not all Either implementations will have a fold.

Also I thought there was some discussion of allowing "Either-like" types instead of just "Either" (I may have misunderstood that, though)

safareli commented 8 years ago

Yah not all of them have fold but it could be fixed with PRs.

For tailRecM to work, the value in m should works as valid fold of Either, even this will be ok:

const Either = {
  Left: (a) => ({ fold: (l, r) => l(a)}),
  Right: (a) => ({ fold: (l, r) => r(a)}),
}

Also there is open PR in purescript-tailrec to use data Step a b = Loop a | Done b instead of Either.

scott-christopher commented 8 years ago

About passing destructor to tailRecM, the example @scott-christopher provided is a bit incorrect

Well spotted. I've shuffled that block of code around too many times today :)

Oh yeah! That's easily fixed with a runIdentity though, right? Or is there a bigger problem?

Right, I've updated the code in the comment to reflect.


Regarding the type of the initial value and the return type being the same, perhaps another way for me to try to explain my thoughts is to ask what would you expect the type to be of the earlier example you gave for getValue.

const getValue = (e) => e.cata({
  Right: (v) => v,
  Left: (v) => v,
})

The only way (at least AFAICT) to produce a return value of c when given an a in (a -> c) from (a -> c) -> (b -> c) -> t a b -> c would be to have the tailRecM logic recursively called from within a -> c until b -> c eventually gets called, but I suspect that would undo the ability to provide the stack-safe optimisations (though I'm not entirely confident my assumption here is correct).

rjmk commented 8 years ago

@safareli For sure. Either way works. I'd personally prefer to provide the fold function than wrap objects, but not hugely strong feelings.

Another option would be to add a need for fold or cata to all the types defined in the spec. I believe, because JS is strict (maybe generators change this), that every type can support a catamorphism (because of being an initial algebra). Alternatively, we could have Data and CoData typeclasses, I think. But I'm definitely out of my depth here

rjmk commented 8 years ago

@scott-christopher Isn't c going to be m (t a b) (from a -> m (t a b) in the signature)?

safareli commented 8 years ago

@scott-christopher we should avoid recursion in tailRecM to solve the problem for which we need tailRecM :d. my first example is not be that well typed. it's just demonstrates that user could use any Either as long as it has correct fold/cata

safareli commented 8 years ago

@garyb

Expanding on the above, we probably could have a Bind m <= BindRec m & (Monad m, BindRec m) <= MonadRec m hierarchy, with the bind and return parts of the morphism laws split between the too, it's just never come up in discussion before as I think the requirement to only satisfy Bind while providing Rec is uncommon as far as we've seen so far

Don't quite get it, are there some laws related to MonadRec? if we would haveBindRec then why would we need MonadRec

garyb commented 8 years ago

@safareli actually, disregard what I said, MonadRec doesn't lift so that whole thing about morphism laws is wrong, I had MonadEff on the brain for some reason, where it does apply.

It seems that BindRec would indeed be good enough to me too.

SimonRichardson commented 8 years ago

I like how we've managed to rope @garyb into this. Welcome to the darkside! 👋

safareli commented 8 years ago

So we can call the interface ChainRec and the method just tailRec as in js this function will be a static method of some Type and adding M is not actually needed AFAICT

safareli commented 8 years ago

as PS is sort of moving to custom ADT which expresses intent more clearly I think We are I think left with this: 1:

type RecStep a b = Next a | Done b
class Chain m <= ChainRec m where
  tailRec :: (a -> m (RecStep a b)) -> a -> m b
  tailRecNext :: a -> RecStep a b
  tailRecDone :: b -> RecStep a b

2:

class Chain m <= ChainRec m where
  tailRec :: ((a -> c) -> (b -> c) -> a -> m c) -> a -> m b

And i'm starting to like the last one

safareli commented 8 years ago

And with #2 if user wants to use Either they could just to map(e => e.fold(next,done)) and return the result. So in my case if i want to leave resume as is, I could just do a map

Free.prototype.foldMap = function(f, m) {
-  return m.tailRecM((v) => v.resume(f, m))(this)
+  return m.tailRecM((next, done, v) => v.resume(f, m).map(e => e.fold(next, done))(this)
}
SimonRichardson commented 8 years ago

number 2 looks the most appealing to me as well tbh.

safareli commented 8 years ago

Looks like everyone is ok with #2 will create PR tomorrow 🎉

rjmk commented 8 years ago

I think it's a shame to provide the constructors rather than ask for the destructors but I admit it's quibbling.

My only concern is with the signature ((a -> c) -> (b -> c) -> a -> m c) -> a -> m b. I can't think of types other than t a b that could satisfy the c slot, but unless we're sure we want to allow them, should we replace c with t a b?

edit: Just noticed the spec doesn't have type signatures, so perhaps this is irrelevant

scott-christopher commented 8 years ago

I can't think of types other than t a b that could satisfy the c slot

In all likelihood, I imagine most implementations would end up using something equivalent to Either a b, but there's no reason that I can see why a t b a or a t a b c couldn't be used instead and provide the same functionality. I have a preference to leave it at c in order to just leave it up to the choice of the implementation, but I'd have no major concerns declaring it as t a b if others prefer. But like you say, unless the type signatures become part of the FL specs soon, this is a bit of a moot point.

rjmk commented 8 years ago

Good point about the t a b c. My concern is resolved!

scott-christopher commented 8 years ago

Is there a preference to either tailRec or chainRec for the method name?