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

Add ChainRec type class #151

Closed
safareli opened this issue Aug 31, 2016 · 65 comments
Closed

Add ChainRec type class #151

safareli opened this issue Aug 31, 2016 · 65 comments

Comments

@safareli
Copy link
Member

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

@paf31 @garyb your suggestions will be helpful on this

@safareli safareli changed the title add TailRec type class Add TailRec type class Aug 31, 2016
@scott-christopher
Copy link
Contributor

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

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

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

Also 👍 on this.

@rpominov
Copy link
Member

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

safareli commented Aug 31, 2016

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

Folds all the way down.

@safareli
Copy link
Member Author

safareli commented Aug 31, 2016

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

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

@safareli
Copy link
Member Author

safareli commented Aug 31, 2016

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

b imo.

@scott-christopher
Copy link
Contributor

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

rjmk commented Aug 31, 2016

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

rpominov commented Aug 31, 2016

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

rjmk commented Aug 31, 2016

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

rpominov commented Aug 31, 2016

@rjmk That also would work.


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

@safareli
Copy link
Member Author

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

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

rpominov commented Aug 31, 2016

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

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

safareli commented Sep 1, 2016

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

safareli commented Sep 1, 2016

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

  • Plus:
    • Looks nicer? ( a bit subjective)
  • Minus
    • It would be hard for IDE/type-checkers to properly understand tailRecM function (or for user to actualy add correct type)
    • In implementation code depends on order, tailRecM need to be defined before it's mutated

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

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:

  • next(a)
  • done(b)
    or
  • Left(e)

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

joneshf commented Sep 1, 2016

This discussion is amazing!

@rjmk
Copy link
Contributor

rjmk commented Sep 1, 2016

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

scott-christopher commented Sep 1, 2016

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

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

rjmk commented Sep 1, 2016

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?

@rjmk
Copy link
Contributor

rjmk commented Sep 1, 2016

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

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

rjmk commented Sep 2, 2016

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

@scott-christopher
Copy link
Contributor

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

@safareli
Copy link
Member Author

safareli commented Sep 2, 2016

@rjmk about c vs t a b, as there are no constraints on t for client there is not a big difference if we say it's c. also this c/ t a b is created used by only tailRec and i think it's better to say as little about it as possible so client's cant do anything with them except wrapping in m. But mostly it would be of type t a b

Also I thought about passing constructors to func vs providing destructor to tailRec and there is one good use case of providing destructor to tailRec.

const tailRecWithFold = (m,f,i) => m.tailRec((done,next,v) > v.fold(done,next),f,i)

Can you sketch what are advantages disadvantages with providing destructor vs passing constructors?

@safareli safareli changed the title Add TailRec type class Add ChainRec type class Sep 2, 2016
@rjmk
Copy link
Contributor

rjmk commented Sep 2, 2016

@safareli Yep, you're completely right about c vs t a b. I guess the thing I was thinking was similar to my thought for fold -- that is it's nice to encode the ability to fold on the 2 type parameters. But I guess the signature for tailRec and maybe some laws sorts us out for that.

On destructors vs constructors, the 2 main advantages I see are

  1. It sort of gives a Church encoding for any Either-like data type, fixing only the interface we care about. It even gives us the ability to show how there's an Either embedded in our type, even if it's a 'larger' type
  2. It saves us from 2 lines of duplication throughout the ecosystem. Not practically a big deal, but philosophically a bit painful

As a disadvantage, I think the API is less familiar to Javascript though, whereas the two callbacks approach is a bit more common.

Anyway, I don't think it's terribly important which we go for

@safareli
Copy link
Member Author

safareli commented Sep 2, 2016

@scott-christopher hypothetically if we would have some other *Rec interfaces MonoidRec or some WhateverRec then with tailRec as a method name we would have conflict. maybe because of that the name is tailRecM in PS to indicate it's Monadic. so for that reason chainRec LGTM and possible if there would be MonoidRec, ApplicativeRec or WhateverRec we would have whateverRec methods without conflicts.

What others think on this?


And also what are thoughts on passing destructors?

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

VS

class Chain m <= ChainRec m where
  -- or tailRec
  chainRec :: ((a -> z) -> (b -> z) -> t a b -> z) -> (a -> m (t a b)) -> a -> m b
  -- or    :: ((a -> z) -> (b -> z) -> t     -> z) -> (a -> m  t     ) -> a -> m b

@rpominov
Copy link
Member

rpominov commented Sep 2, 2016

+1 for chainRec it is also consistent with Chain algebra that has chain method.

And +1 for constructors because they are simpler to use especially if we don't have an Either type on hand:

T.chainRec((next, done, x) => {
  return x > 0 
    ? T.of(next(x - 1)) 
    : T.of(done(x))
}, initial)

// I have to create an ad-hoc Either in order to use `chainRec`
T.chainRec((next, done, result) => 'next' in result ? next(result.next) : done(result.done), x => {
  return x > 0 
    ? T.of({next: x - 1}) 
    : T.of({done: x})
}, initial)

// With an existing Either type it's also a bit more complicated than with constructors
T.chainRec(Either.fold, x => {
  return x > 0 
    ? T.of(Either.right(x - 1)) 
    : T.of(Either.left(x))
}, initial)

@rjmk
Copy link
Contributor

rjmk commented Sep 2, 2016

@rpominov One can always define a function with the next / done API around one with the destructor. There's no way to get the constructor version to stop using its version of Either (though you can just fold your version down with the constructors)

I really hope we get somewhere to keep our 🚲 s! I'm reasonably confident that its better to build it from oak than yew, but I don't think the difference is large

@safareli
Copy link
Member Author

safareli commented Sep 2, 2016

@rpominov one can write this with desctructor version:

const chainRec = (m, f, initial) => m.chainRec(
  (next, done, v) > v.fold(next, done),
  (v) => f(
    // or Either.Left
    (a) => ({ fold: (done, next) => next(a) }),
    // or Either.Right
    (a) => ({ fold: (done, next) => done(a) }),
    v
  ),
  initial
)

@rpominov
Copy link
Member

rpominov commented Sep 2, 2016

So you're saying that if we have chainRec based on destructors we can build a chainRec based on constructors? But this is also true other way around:

const chainRec = (fold, f, initial) => m.chainRec((next, done, x) => {
  return f(x).map(a => fold(next, done, a))
}, initial)

I just don't understand what we can do if we'll be able to use our own Either type? What additional possibilities this opens?

@rjmk
Copy link
Contributor

rjmk commented Sep 2, 2016

Yeah, sorry that's what I meant here:

There's no way to get the constructor version to stop using its version of Either (though you can just fold your version down with the constructors)

I guess my point there was that you always need constructors and destructors. With destructors you can do some wrapping to make the API the constructor way and only have one pair of constructors and one destructor. If you have internal constructors you end with 2 destructors and 2 pairs of constructors

@rpominov
Copy link
Member

rpominov commented Sep 2, 2016

If you have internal constructors you end with 2 destructors and 2 pairs of constructors

Sorry, not sure if i understand this right. You mean that if we consider two versions of chainRec converted from spec API to alternative API (1: #151 (comment) and 2: #151 (comment)). The #2 version will have more wrapping/unwrapping under the hood? So it would hurt performance?

Not sure we should care about performance here, but also I just don't understand why one would want to use their own Either, so why would they do #2 conversion even though it's possible?

@rjmk
Copy link
Contributor

rjmk commented Sep 2, 2016

Definitely not advocating it because of performance (though you're right that would be the obvious impact)! More philosophy. The ChainRec doesn't need to know how to build and destroy Eithers, it just needs to know how to check if a computation is finished (in a sensibly typed way). We can encode that need in the destructor and then I think the ChainRec is more focused on what it's actually about.

also I just don't understand why one would want to use their own Either

That's fair. The main case I'm thinking of is if one already has a function in terms of Eithers (or Futures, or ...) that could be recursed on. Not sure how common that would be

@safareli
Copy link
Member Author

safareli commented Sep 2, 2016

If we T has some internal Either like structure then users does not need to have it too and could just use done/next functions instead of depending on some other Either and passing fold.

@rjmk
Copy link
Contributor

rjmk commented Sep 2, 2016

Basically I think that that passing the constructors is "easier" and asking for the destructors is "simpler" (in the Rich Hickey sense)

@safareli
Copy link
Member Author

safareli commented Sep 3, 2016

If user wants to use the chianRec then, destructor version asks user for some structure which they might only need for using chianRec and not elsewhere and a fold function for that. Also those structures would still need to implement the Either like thing for testing.

When constructor version passes everything needed to actually use the function and user need nothing else (works out of the box). also this way the structure could have some efficient representation of Next/Done and user would not have to think on that at all. I think we could go with constructor version as it would be easy to use. also PS version is going with ADT version for future and constructor version is more like that.

@safareli
Copy link
Member Author

safareli commented Sep 3, 2016

Also with destructor case, asking to pass fold as argument is basically same as asking the object to have the fold method

@rjmk
Copy link
Contributor

rjmk commented Sep 3, 2016

Also with destructor case, asking to pass fold as argument is basically same as asking the object to have the fold method

Yeah maybe that would be the more "Fantasy Land" way.

Anyway, I think you should make the PR with the constructors. 👍

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

No branches or pull requests

7 participants