Closed safareli closed 8 years ago
@paf31 @garyb your suggestions will be helpful on this
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)
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
)
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 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.
With fold
, isDone
and getValue
would change to:
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.
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.
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 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 |
b
imo.
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.
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
.
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 :)
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')))
@rjmk That also would work.
Edit: Although we wouldn't be able to use Future.of
and Future.rejected
as next
and done
.
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?).
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
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...
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
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
).
There are two main directions:
Either
(fold
or cata
)done/next
as Type.tailRecM{Next,Done}
done/next
as Type.tailRecM.{next,done}
done/next
as arguments to function
passed to Type.tailRecM
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 argumentarg
of typea
. theM.tailRecM
will invokefunc
with value of typea
; it will take value out, from its returned value of typem (Either a b)
; if it'sLeft a
, it will callfunc
again with the value of typea
, until there is sees valueRight b
and in that caseM.tailRecM
will terminate and returnm b
.To construct
Right/Left
values use any implementation ofEither
type which has methodfold
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 hasfold
)- 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 usetailRecM
you need to import or implement something yourself
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 argumentarg
of typea
. theM.tailRecM
will invokefunc
with value of typea
; it will take value out from its returned value of typem (TailRecRes a b)
; if it'sNext a
, it will callfunc
with value of typea
again, until there is valueDone b
and in that caseM.tailRecM
will terminate and returnm b
.To construct
Next/Done
values useM.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 hasfold
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
tailRecM
function (or for user to actualy add correct type)tailRecM
need to be defined before it's mutated2.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 argumentarg
of typea
. theM.tailRecM
will invokefunc
with value of typea
and two functionsnext
of type(a -> c)
anddone
of type(b -> c)
; it will take value, out from its returned value of typem c
; if it was created withnext
it will callfunc
again with the value of typea
(using which thec
was created); if it was created usingdone
then it will terminate with value of typem 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 argumentarg
of typea
. theM.tailRecM
will invokefunc
with value of typea
; it will take value out, from its returned value of typem (Either a b)
; if it'sLeft a
, it will callfunc
again with the value of typea
, until there is sees valueRight b
and in that caseM.tailRecM
will terminate and returnm b
.To construct
Left/Right
values useM.tailRecMNext/M.tailRecMDone
respectively, or any implementation ofEither
which has methodfold
of type(a -> c) -> (b ->c) -> c
.
tailRecMNext/Done
could be used to constructEither
values if user does not use anyEither
in a project.- User can use favorit implementation of
Either
(as long it hasfold
)- 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
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)
orLeft(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.
This discussion is amazing!
@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
@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 (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).
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?
@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
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.
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
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
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 inm
we haveEither 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)
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
.
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).
@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
@scott-christopher Isn't c
going to be m (t a b)
(from a -> m (t a b)
in the signature)?
@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
@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
@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.
I like how we've managed to rope @garyb into this. Welcome to the darkside! 👋
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
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
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)
}
number 2 looks the most appealing to me as well tbh.
Looks like everyone is ok with #2
will create PR tomorrow 🎉
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
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.
Good point about the t a b c
. My concern is resolved!
Is there a preference to either tailRec
or chainRec
for the method name?
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