-
Notifications
You must be signed in to change notification settings - Fork 377
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
Comments
Regarding 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 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) |
The church notation is basically a fold method of Either. |
I would love to see the usage of fold, it's more generalised imo. Also 👍 on this. |
What if instead of relying on an unified Either interface the type itself would provide methods that would create So usage would look something like this:
One reason why this might be a better approach is because many Future/Task implementations have built-in Either. |
With const isDone = (e) => e.fold(() => false, () => true)
const getValue = (e) => e.fold((v) => v, (v) => v) |
Folds all the way down. |
@rpominov that's also interesting. 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 |
This is very reminiscent of a trampoline and has worked well there already, so I'm not against that idea either. |
In the sense of accessibility, using done/next is better for newcomers, and even ones who understand So now question is, which one should be used:
|
|
Another option would be to provide the 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. |
If we want to avoid the 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 |
Actually With Either we have more freedom of how to create return values of recursive function. We can do 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 Future.tailRecM((x, next, done) => {
return someCondition(x) ? makeNetworkRequest().chain(next) : done('done')
}) So just ignore the point I've made, sorry :) |
Would there be any issue with providing the internal Future.tailRecM((x, next, done) =>
someCondition(x) ? makeNetworkRequest.map(next) : Future.of(done('done'))) |
@rjmk That also would work. Edit: Although we wouldn't be able to use |
One thing with getting |
We end up with the same type when using the church-encoding or 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 For example, the following signature says that the only way to construct the tailRecM :: MonadRec m => (a -> (a -> c) -> (b -> c) -> m c) -> a -> m b An implementation is then free to specialise to use tailRecM :: MonadRec m => (a -> (a -> Either a b) -> (b -> Either a b) -> m (Either a b)) -> a -> m b |
Yeah, I just was thinking about another signatures for next/done —
But also |
I'm still mulling it over, but I think I agree. I also think My only other topic of bikeshedding here is that I'm not particularly sold on the name One suggestion for another name would be class (Chain m) <= ChainRec m where
chainRec :: (a -> (a -> m c) -> (b -> m c) -> m c) -> a -> m b |
it's not quite true for example for |
There are two main directions:
Pluses and Minuses of
|
This still doesn't prohibit it as an option to use 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 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
This is a valid point (likewise with the use of
The same types are effectively in play whether we provide them to the function or require the end user to source them elsewhere.
This could perhaps be tidied up by placing the I guest my personal preference would still be leaning towards having the constructors provided to the function as arguments (whether that is |
This discussion is amazing! |
@scott-christopher @safareli What do you think about providing a destructor to 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 It would look like tailRecM
:: MonadRec m => ((a1 -> c1) -> (b1 -> c1) -> t a1 b1 -> c1)
-> (a -> m (t a b)) -> a -> m b |
@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 |
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 I don't think that's necessary a showstopper though, as it's just a |
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 Identity.tailRecM
( Either.either
, x => somePred(x) ? Either.Right(someF(x)) : Either.Left(someG(x))
, someVal
) And then you might as well extract |
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 edit: Just noticed the spec doesn't have type signatures, so perhaps this is irrelevant |
In all likelihood, I imagine most implementations would end up using something equivalent to |
Good point about the |
Is there a preference to either |
@rjmk about Also I thought about passing constructors to 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 Yep, you're completely right about On destructors vs constructors, the 2 main advantages I see are
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 |
@scott-christopher hypothetically if we would have some other 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 |
+1 for 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) |
@rpominov One can always define a function with the 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 |
@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
) |
So you're saying that if we have 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? |
Yeah, sorry that's what I meant here:
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 |
Sorry, not sure if i understand this right. You mean that if we consider two versions of 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 |
Definitely not advocating it because of performance (though you're right that would be the obvious impact)! More philosophy. The
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 |
If we |
Basically I think that that passing the constructors is "easier" and asking for the destructors is "simpler" (in the Rich Hickey sense) |
If user wants to use the 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. |
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. 👍 |
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
MonadRec
type class looks like this:So we could have something like this without stack overflow:
one thing is that there are multiple
Either
implementations in js and this spec should depend on bare minimum from anyEither
type. from what I have found the minimum api from Either type is to have acata
method. but before that let's see an implementation oftailRecM
ofIdentity
:So we have seen usage of it and part of it's implementation. now what we have left is implement
isDone
andgetValue
so that they are as generic as possible.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 hascata
).To note this Either implementation would work with
tailRecM
.The hardest was to implement
tailRecM
forTask/Future
but i have made it and after we agree on some interface I would create PRs for some popularTask/Future
implementationsThe text was updated successfully, but these errors were encountered: