question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Update `chainRec`

See original GitHub issue

I 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:open
  • Created 6 years ago
  • Reactions:6
  • Comments:18 (9 by maintainers)

github_iconTop GitHub Comments

5reactions
i-am-tomcommented, May 22, 2017

I should clarify: I understand (and use in production) chainRec. However, to address those points in order:

I don’t see any inconsistencies, just some layers of abstraction.

There’s a misuse of the term “abstraction” here. It’s one abstraction that hides the relationship between chain and chainRec, which we’d ideally like to make as clear as possible. It’s moving away from “abstraction” and more towards “obfuscation” in this regard.

I get along with chainRec quite well

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 😦

I was able to implement chainRec with a very minimal type for c.

No matter how minimal your type, it simply doesn’t need to exist in the case of Either or Result, which could be implemented as the simplest of sum types. Why would you have to implement Either 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 and rej) and use that as a constraint, which would mean you wouldn’t need a separate inner type at all!

c is a wrapper over a b

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 the done: 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”, and a -> c looks like total magic to someone trying to understand parametricity ✨

You would have to first MyChainRec.of(‘a’)

No; this would make MyChainRec a monad (more specifically a MonadRec). 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 😦

3reactions
Avaqcommented, May 22, 2017

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.

there are some inconsistencies to fix with chainRec

The chainRec spec is not inconsistent, right? It’s just several layers of abstraction which may be difficult to grasp. How is it inconsistent?

Every time I use chainRec, I have to look at MonadRec in Scala or PureScript to remember how it works […] I can only do this because of my limited knowledge of those languages, though!

I have no other reference than the FantasyLand spec, and I get along with chainRec quite well.

Using Either makes this type much more comprehensible to beginners.

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.

“where does the b come from?”

c is a wrapper over a b. You are given two functions to wrap your a or b in a c, 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 of chainRec (m b). Maybe clarifying the fact that c is a wrapper type would help:

chainRec :: ((a -> c a d, b -> c d b, a) -> m (c a b), a) -> m b

I’m not sure if that’s proper, but at least now you can see that c is just another word for Either, but generalized to any “bi-wrapper” type.

if we did make [chainRec] an instance method, we wouldn’t need the a parameter on the inner function as we’d already have it!

Not quite, the second parameter is of type a, whereas it would be of type m a if chainRec would be a method. You would have to first MyChainRec.of('a') before you can use chainRec, 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:

//    recur :: Integer -> Integer
const recur = x => x > 10000 ? M.of(x) : M.of(x + 1).chain(recur);
//    recur :: Integer -> Integer
const recur = M.chainRec((next, done, x) => M.of(x > 10000 ? done(x) : next(x + 1)));
Read more comments on GitHub >

github_iconTop Results From Across the Web

MengtingWan/chainRec - GitHub
chainRec. This repo includes a tensorflow implementation of the algorithm -- chainRec, proposed in the paper. Mengting Wan, Julian McAuley, ...
Read more >
ChainRec.ts - fp-ts
Functional programming in TypeScript.
Read more >
Fantas, Eel, and Specification 14: ChainRec - Tom Harding
We store our acc , starting with the “empty” value for that Monoid , and update it with every loop. This continues until...
Read more >
Item Recommendation on Monotonic Behavior ... - UCSD CSE
Algorithm 1 chainRec for each user u, and each item i ∈ I. + u do. Locate the last positively interacted stage l∗...
Read more >
How to implement a stack-safe chainRec operator for the ...
1, loop and done are the chaining functions, and so the chainRec ... With the cont flattening patch in place, everything else works....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found