Update `chainRec`
See original GitHub issueI know I’m not the first to bring this up - this has been discussed in #151, #152, #185, and probably others - but there are some inconsistencies to fix with chainRec
that are undoubtedly best addressed sooner rather than later. I hope this all makes sense!
I’ve been writing a series of posts on Fantasy Land spec, but I’ve hit a wall with this one, as I think we’ve made it a bit harder than we should’ve, so I’d like to propose we do something about it 😄 tl;dr, my proposed signature is this:
chainRec :: ChainRec m => m a ~> (a -> m (Either a b)) -> m b
Either
Using Either
makes this type much more comprehensible to beginners. Every time I use chainRec
, I have to look at MonadRec
in Scala or PureScript to remember how it works, and then mentally translate it to the current approach, and I know I’m not the only one 😳 I can only do this because of my limited knowledge of those languages, though!
In production, we’ve simply taken to using something like the following. Forgive the name - it was a bad homonym joke that apparently caught on…
//+ trainWreck :: ChainRec m => (a -> m (Either a b)) -> ((a -> c, b -> c, a) -> m b)
const trainWreck = f => (next, done, x) =>
chain(cata({ Left: next, Right: done }), f(x))
It works, but no one who hasn’t seen Haskell/PS/Scala understands why 😞 The lack of understanding isn’t totally due to this, though - there are two other problems…
m b
The eventual return of chainRec
is m b
. If we imagine writing the type in PureScript/RankNTypes
, we’d get here:
chainRec :: ChainRec m => forall a. (forall b c. (a -> c, b -> c, a) -> m c, a) -> m b
The b
type gets introduced in our inner function, and then reappears in the return value of the outer function. For a strict type system, one of these doesn’t know what b
is. Either b
is declared in outer scope, and the inner function has no idea what b
is (which makes writing such a function really hard), or it’s declared in inner scope, and can’t be returned from outer scope! @joneshf mentioned this somewhere (“where does the b
come from?”), and it’s certainly another point of confusion for newcomers. Of course, with an Either
, none of this matters - positive and negative positions, etc. Again, @joneshf did a much better job than I will of explaining this, hah!
m a ~>
The spec entry talks about a value implementing the ChainRec
spec, but chainRec
is a static function (vs. chain
that is at the instance-level). Firstly, this creates some confusion as it implies they work in similar ways. Secondly, if we did make it an instance method, we wouldn’t need the a
parameter on the inner function as we’d already have it! Given that we only use it once - at the start - and we know m
is a chain
type, we can safely assume that the user can always get to an m a
, and is probably more likely to have started there. Of course, it’s not as simple as pure
(which I assume is why other specs call this MonadRec
), but we do have a function at our disposal that will lift an a
inside an m
or give us the end result!
I know there has been mention of the ordering within Either
, so I guess this would be a good change to add to the end (were this proposal accepted!), but these are, as I see it, the main worries. Of course, it seems that none are original thoughts on my part, but I think it’s probably worth addressing them collectively!
Thanks 😃
Issue Analytics
- State:
- Created 6 years ago
- Reactions:6
- Comments:18 (9 by maintainers)
Top GitHub Comments
I should clarify: I understand (and use in production)
chainRec
. However, to address those points in order:There’s a misuse of the term “abstraction” here. It’s one abstraction that hides the relationship between
chain
andchainRec
, which we’d ideally like to make as clear as possible. It’s moving away from “abstraction” and more towards “obfuscation” in this regard.You are certainly more capable than me, then 🙇 However, my experience with trying to introduce this approach to programmers without a functional background hasn’t been nearly so positive 😦
No matter how minimal your type, it simply doesn’t need to exist in the case of
Either
orResult
, which could be implemented as the simplest of sum types. Why would you have to implementEither
yourself?Conceivably, in the case of fluture, a better approach would have been to introduce a typeclass along the lines of
MonadError
(i.e. a way of noting “success” and “failure” -res
andrej
) and use that as a constraint, which would mean you wouldn’t need a separate inner type at all!Careful here: for a start, your type should be
(a -> c a b, b -> c a b, a) -> m (c a b), a) -> m b
, which is definitely more confusing. The fact is that thedone: true
flag exists only to mimic a sum type. If we introduced a spec that said(a -> m (n a b))
“for some bifoldable n”, then we’re in total agreement here, and I’d be cool with that as a middle ground. However, “bifoldable” is definitely less beginner-friendly than “either”, anda -> c
looks like total magic to someone trying to understand parametricity ✨No; this would make
MyChainRec
a monad (more specifically aMonadRec
). The easiest way to “lift” it is to run your bifoldable-returning function, which is exactly the plan at every other step!I think, ultimately, my concern comes from the fact that we run into troubles that no other community seems to have, simply because of this encoding. The ranks involved mean that you either have to say “don’t look too closely at this”, not teach people how the type signatures work, or explain higher-rank polymorphism. Regrettably, most will go for the first option, and that’s not going to do any of us any good in the long run 😦
In defense of chainRec: I’m not here to dismiss your ideas, but rather to present the other side of the story. I don’t have a background in any kind of functional programming language, and purely as a JavaScript programmer consider ChainRec to be quite fine.
The chainRec spec is not inconsistent, right? It’s just several layers of abstraction which may be difficult to grasp. How is it inconsistent?
I have no other reference than the FantasyLand spec, and I get along with chainRec quite well.
I (a beginner) was quite happy that the FantasyLand spec doesn’t push any specific types onto me. I was able to implement chainRec with a very minimal type for
c
.If I had to use an Either type, I would have to implement it myself, bothering my users with a partially implemented Either if I didn’t go all the way, or use an existing implementation, which brings in a whole new dependency just to get
chainRec
working which is meant to be abstracted over any way.I worry that requiring authors to use Either will lead to many different implementations of the Either type being created.
c
is a wrapper overa b
. You are given two functions to wrap youra
orb
in ac
, and are expected to do so as indicated by the return type of your function (m c
). It is then unwrapped before it’s returned, as indicated by the return type ofchainRec
(m b
). Maybe clarifying the fact thatc
is a wrapper type would help:I’m not sure if that’s proper, but at least now you can see that
c
is just another word forEither
, but generalized to any “bi-wrapper” type.Not quite, the second parameter is of type
a
, whereas it would be of typem a
ifchainRec
would be a method. You would have to firstMyChainRec.of('a')
before you can usechainRec
, like you mentioned. But I don’t think wrapping a value for the sake of accessing a method which may as well have been a normal function is good practice. With currying in mind, I think the current argument order is rather convenient: the following two functions are “equal”, except the first might not have constant stack usage: