Closed safareli closed 8 years ago
@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 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
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 *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 M
onadic. 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
+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)
@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
@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 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?
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
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: https://github.com/fantasyland/fantasy-land/issues/151#issuecomment-244375062 and 2: https://github.com/fantasyland/fantasy-land/issues/151#issuecomment-244378085). 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?
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
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
.
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 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.
Also with destructor case, asking to pass fold as argument is basically same as asking the object to have the fold method
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 withtailRecM
.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
implementations