-
Notifications
You must be signed in to change notification settings - Fork 376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update chainRec
#250
Comments
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 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 I worry that requiring authors to use Either will lead to many different implementations of the Either type being created.
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
Not quite, the second parameter is of type // 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))); |
I should clarify: I understand (and use in production)
There's a misuse of the term "abstraction" here. It's one abstraction that hides the relationship between
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 Conceivably, in the case of fluture, a better approach would have been to introduce a typeclass along the lines of
Careful here: for a start, your type should be
No; this would make 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 :( |
Signature with forall should look like this:
So it's not true to say "where It's almost The function exists just so that you get stack safety and it could be abstracted away using Free monad like structures (for example if you do recursive chain using fluture you are safe). and indeed it's somewhat obfuscated (to don't ask for concrete implementation of Either or Step(this is used in PS)). Using |
Intuitively, a type-checker couldn't rectify that type because there's no way (via parametricity) to get from Again, I totally understand what it's meant to accomplish, although I'm not sure it requires the Free hammer in order to be useful. My point about @safareli, thoughts on making |
if you make it instance method then you should also require structure to be a Monad otherwise it will be more limiting then what we have now. and if you require monoid then structures which are not monad but are chain couldn't implement it so we loos number of instances which can exist. |
The first step of the loop is |
|
if you think step function |
but I can still feed I mentioned in my reply to @Avaq that we could introduce Bifoldable and then specify that as a requirement? That way, we wouldn't specifically require |
if chainRec requires Monad then Map can't be actually Bifoldable constraint could be a good way to go. before we thought about having some interface like thing for it but actually no one mentioned Bifoldable then so we went with function way. class Bifoldable p where
bifoldr :: forall a b c. (a -> c -> c) -> (b -> c -> c) -> c -> p a b -> c
bifoldl :: forall a b c. (c -> a -> c) -> (c -> b -> c) -> c -> p a b -> c
bifoldMap :: forall m a b. Monoid m => (a -> m) -> (b -> m) -> p a b -> m I would be happy with chainRec :: ChainRec m => Bifoldable f => ((a -> m (f a b)), a) -> m b
updateit's not quite true as Tuple is bifoldable :/ so we can't use Bifoldable for it |
Ah, that is a damn shame. I guess the only applicable structure is coproduct, which is, well, a sum type :/ Curses! I'll keep thinking. |
I think the underlying problem is that the spec doesn't specify the short circuiting mechanism (i.e. how to trigger the base case) of |
Again type could be more helpful if we used custom data types: data Step a b = Loop a | Done b
tailRecM :: forall a b. (a -> m (Step a b)) -> a -> m b but we don't |
Why not do something similar to #224?
|
I see, FL is meant for type constraints only, not for types per se. However, with There is no need to use a generic I think Church encoding was already suggested in an earlier related thread. There seems to be a lot discussions on this topic - sorry for bringing it up again. |
Passing constructors to iterator is almost church encoding I think. And one could easily guess which constructor is for |
My 2¢ ... I like the current signature. Here's what I currently have: export interface ChainRec<F> extends Chain<F> {
chainRec<A, B>(f: <C>(next: (a: A) => C, done: (b: B) => C, a: A) => HK<F, C>, a: A): HK<F, B>
} That's actually understandable IMO and not very different from the proposed: export interface ChainRec<F> extends Chain<F> {
chainRec<A, B>(f: (a: A) => HK<F, Either<A, B>>, a: A): HK<F, B>
} I mean, without some documentation to understand how this signature works (e.g. keep calling N.B. There's no way to specify an So either impose an The current encoding is awesome because it has no dependencies on data types. Introduce dependencies on data types and the specification will be much less useful than it currently is. |
@alexandru see #280 and #281 for proposals that get around specifying an implementation. |
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:
Either
Using
Either
makes this type much more comprehensible to beginners. Every time I usechainRec
, I have to look atMonadRec
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...
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
ism b
. If we imagine writing the type in PureScript/RankNTypes
, we'd get here: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 whatb
is. Eitherb
is declared in outer scope, and the inner function has no idea whatb
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 theb
come from?"), and it's certainly another point of confusion for newcomers. Of course, with anEither
, 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, butchainRec
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 thea
parameter on the inner function as we'd already have it! Given that we only use it once - at the start - and we knowm
is achain
type, we can safely assume that the user can always get to anm a
, and is probably more likely to have started there. Of course, it's not as simple aspure
(which I assume is why other specs call thisMonadRec
), but we do have a function at our disposal that will lift ana
inside anm
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 :)
The text was updated successfully, but these errors were encountered: