-
Notifications
You must be signed in to change notification settings - Fork 376
Description
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 bEither
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 bThe 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 :)