Skip to content

Update chainRecΒ #250

@i-am-tom

Description

@i-am-tom

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 :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions