Skip to content

CoroutineT #3

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

Closed
wants to merge 4 commits into from
Closed

CoroutineT #3

wants to merge 4 commits into from

Conversation

paf31
Copy link
Contributor

@paf31 paf31 commented May 18, 2014

@garyb @puffnfresh @jdegoes @joneshf

Moving my MonadBounce type class to a branch for more work.

This is just for discussion. I'm happy to bin this if we decide that Gosub is the way to go, but I would still make a claim that both this and Gosub should have a place in the std lib.

I think Gosub essentially reifies the closure which happens when using bounce, except that Gosub uses a bounce on every call to >>=. Also, Gosub can be implemented in terms of BounceT, but not vice versa.

evalBounceTEff is still a victim of the tail call optimizer.

@jdegoes
Copy link

jdegoes commented May 18, 2014

Having to manually call bounce continuously or risk exploding the stack seems like a tremendous amount of cognitive overhead (requiring great discipline and careful forethought!) as well as distinctly low-level for a language like Purescript.

I could understand it if it were, say, 10x faster than Gosub, but I can't see performance being substantially different (certainly not 10x). If performance is not the benefit, then what else? The fact that it's a transformer?

With @puffnfresh's Task, we have an Eff with unbounded monadic recursion, and it would be easy to define all base monads (including those like type State = StateT Id) as transparently running on Trampoline so Purescript's standard libraries would be safe out of the box. Only Eff would be unsafe, but we could move Task somewhere more prominent and possibly rename it to something like EffRec.

Essentially, I'm opposed to decisions and libraries that increase cognitive overhead, increase the ways in which programs can go wrong, and add surprising, inconsistent behavior (you can imagine the Reddit / Hacker News commentary on the need to call bounce every few lines!).

Purescript already has great escape hatches when performance is necessary (FFI, Eff). The rest of Purescript should have very low cognitive overhead, reduce the ways in which programs can go wrong, and be consistent, unsurprising, even boring.

And that's my 2 cents. :)

@paf31
Copy link
Contributor Author

paf31 commented May 18, 2014

@jdegoes I understand what you're saying, and I'm not necessarily disagreeing with you, but I would say the following:

  • We can achieve the same thing with Bounce by writing the Gosub version of Free in terms of BounceT. Gosub merely reifies the closure created when bounce is called, but it does so on every call to bind. Simply write
data AlwaysBounce m a = AlwaysBounce (BounceT m a)

instance alwaysBounce :: (Bind m) => Bind (AlwaysBounce m) where
  (>>=) m f = m >> AlwaysBounce bounce >>= f

and you get the same effect. The programmer just has to pick the right monad depending on how much control they want.

  • The user does not have to bounce every few lines. It it enough usually to bounce before every recursive call. In fact, you can write the following:
withTrampoline :: forall m a. (MonadBounce m) => m a -> m a
withTrampoline action = do
  bounce
  action

and the fib example can just be written as

go 1 = pure 1
go 2 = pure 1
go n = withTrampoline do
  a <- go (n - 1)
  b <- go (n - 2)
  return (a + b)

where the bounces are all stuck on the front of fib.

  • As far as I can tell, Free with Gosub isn't even a Monad according to the laws. This is what bothers me the most. While it works, it is, at its core, a hack around the limitations of Javascript, and I'm not sure I want to advertise that one hack is better than another.

I feel like programming with Free and mtl-style classes and transformers is just one way to code in Purescript. The React bindings are a good example of another style entirely. I just don't want to bake any one particular style into the standard libraries.

Also, I didn't quite understand your comment about Task. I haven't really seen an example of it yet, so I don't really understand it's utility, but I would like to.

@jdegoes
Copy link

jdegoes commented May 19, 2014

This is an unsafe interface because your code will explode if you fail to call "bounce" at the appropriate junctions. Couldn't we expose the same functionality through a sort of "monadic Y combinator", in which a recursive call would automatically perform the bounce? That might provide the (possible) performance benefits of bounce without the externally unsafe API.

Also, I am not arguing we make MTL the default way of writing functional code in Purescript. With the effect system, it remains to be seen what will end up becoming "idiomatic functional Purescript".

I'm only arguing that with the exception of Eff, the base monads we provide in Purescript libraries should be recursion-friendly. That is, a newbie to Purescript should be able to use all the non-transformer monads (except Eff) without exploding the stack. It's a lot of cognitive overhead to keep track of how many times I can use recurse when programming with State, etc., and from a library perspective, these base monads are already written in terms of their transformers, so switching them over to use a trampoline is extremely easy and shouldn't change the API (I volunteer to make the change).

"Free with Gosub" is equivalent to ordinary (Free (Coyoneda f)) and should satisfy all monad laws. Which laws do you think it violates?

Task is basically Eff but with guaranteed-safe unbounded recursion. I'd recommend giving Task a prominent position in Purescript (by that or a name like EffRec) because writing recursive Effcode is unsafe -- it's not easy to reason about whether or not a given recursive Eff function will blow the stack (nor will most developers new to Purescript want to worry about such things in day-to-day programming).

@puffnfresh
Copy link
Contributor

@paf31 I'd too be very bothered if Gosub violated the monad laws. Does it?

Other than that, I think Free with Gosub is the right thing to do in most cases. We've been doing it in Scala for a long time, for the exact same reasons. For example, our IO is Trampoline[(World[RealWorld], A)].

There was some talk about writing a FreeT and then a TrampolineT but they found that it's only possible to write a stack-safe one when you can access the trampolining of the inner monad. So TrampolineT is totally useless.

@paf31
Copy link
Contributor Author

paf31 commented May 19, 2014

As far as I can tell none of the laws are satisfied equationally, even though they might be satisfied operationally. I'm looking at this line https://github.com/purescript-contrib/purescript-trampoline/blob/master/src/Control/Monad/Free.purs#L11

Sorry, I'm not seeing the connection to Free (Yoneda f).

Here's the gist of my argument: one approach can be expressed in terms of the other, but not the other way around, so why choose the less general approach as the one you want to base the libraries on?

As for Task, I would like to see a couple of examples. Eff should be stack-safe for tail calls as long as TCO works, which is another issue, and we can use CoroutineT Eff to make it stack-safe in the presence of general recursion. As for where Task should live, the general rule is: things which need optimizer support or compiler knowledge go in Prelude.purs, things which we plan to support as part of every release with tests, docs etc. can go in the purescript org, and other interesting things can go in purescript-contrib.

@paf31
Copy link
Contributor Author

paf31 commented May 19, 2014

@puffnfresh Indeed, CoroutineT here only works when you can write a tail recursive run function, which I have included for Thunk and Eff. The general form is useless for removing stack frames.

@paf31
Copy link
Contributor Author

paf31 commented May 19, 2014

I'd also like to say - I don't see any reason why we need to pick just one solution for this. I think both make interesting libraries in their own right, with their own use cases. For making ParserT stack safe, I would personally prefer to use CoroutineT, since I understand where to put the yields and I can save myself some unnecessary computation by adding them manually. But I can see why people would pick Gosub-Free instead as well.

As for Task, I'm happy to advertise it as a preferred solution for monadic recursion if we can show some examples (to be honest, I still don't quite understand it). It's important to note that Eff isn't special in any way other than that its >>= gets inlined by the optimizer (which is why it lives in Prelude.purs). It's just a library. A good way to advertise Task would be to write a blog post.

@paf31 paf31 changed the title Initial BounceT implementation CoroutineT May 19, 2014
@puffnfresh
Copy link
Contributor

@paf31 you're right about the monad laws. This makes me very sad. I'll spend a while trying to figure out what we can do.

@paf31
Copy link
Contributor Author

paf31 commented May 19, 2014

@puffnfresh I think you can convince yourself that they hold if you look at it through the lens which identifies Gosub m f with m >>= f, since Gosub seems to just close over the values it needs to delay the call to >>= (just like yield, really ;))

Edit: I think it's not a monad because under the isomorphism I haven't proved, >>= becomes

(>>=) m f = m >> CoroutineT bounce >>= f

which isn't a monad for the obvious reason.

@paf31
Copy link
Contributor Author

paf31 commented May 19, 2014

One more note: I wrote this mostly to use with parsing, and I wanted to put it out there with the caveat "use this if you know what you're doing". I feel like we should do that for Task, Gosub-Free and Trampoline libraries, rather than giving special status to anything. I feel very strongly that we should try to implement these things in libraries without having to change the compiler.

And when it comes to beginner adoption, I think that using something like StateT has a relatively high barrier to entry anyway, so asking someone to stick withTrampoline on the front of a recursive function should be a fairly reasonable thing to ask. I don't like the idea of anyone using PureScript without a full understanding of what's going on at the Javascript level anyway. If you don't understand the runtime behaviour of StateT without CoroutineT, then you probably should learn it before using it.

@paf31
Copy link
Contributor Author

paf31 commented May 23, 2014

Any more thoughts on this?

@jdegoes
Copy link

jdegoes commented May 23, 2014

Why can't the base monads (such as State, etc.) be baked with trampolining?

If a user wants to use a monad transformer, we can assume they are more sophisticated and will be personally responsible for supporting recursion through their choice of base monad or use of something like CoroutineT.

But users new to Purescript who just import State (currently StateT Id) and write recursive functions that blow up will be very confused and put-off at the lack of stack-safety.

So my vote would be for: keeping CoroutineT and Free w/Gobsub, and redefining base monads to be safe-by-default, by changing MonadT Id to MonadT Free for all MonadT, and creating a run function for the base monads.

@paf31
Copy link
Contributor Author

paf31 commented May 23, 2014

I would rather offer both alternatives as separate type synonyms, just because Free comes with a performance cost, which you might not want if you're not writing recursive code.

It also seems like a little bit of a shame to have a dependency on free from transformers.

Can I not convince you that replacing do with withTrampoline do has a low enough cognitive overhead that it is a valid solution?

I feel like if someone starts using monadic recursion and runs into a stack overflow, it should not be surprising. One of the project goals is to stay as close to Javascript semantics as possible, and you don't get surprised in general when Javascript gives a stack overflow due to recursion. How to fix the problem is a different issue, but I think it should require a conscious step to introduce a trampoline.

@puffnfresh
Copy link
Contributor

I'm currently fixing scalaz.Free to not violate the monad laws:

scalaz/scalaz#721

I'll do the same for my implementation and I can't think of anything other than performance that would then be a problem.

I'm happy for PureScript to have JS semantics as long as those semantics don't have a huge cost. I see having to reason about stack safety as a pretty big cost.

💎

@paf31
Copy link
Contributor Author

paf31 commented May 23, 2014

I agree it might be a big cost, depending on the style of code you're writing.

I would just consider any use of purescript-transformers to be "advanced", so you have to be aware of the runtime behavior of the code in any case.

If we're just talking about a set of type synonyms, it seems like the easiest solution is just to make a new package which depends on both free and transformers, which defines synonyms for State, Writer etc. with the same names (different module) which are equal to StateT Free, WriterT Free etc. and then to recommend that as the package of choice for beginners.

Edit: Still, none of this really invalidates CoroutineT as a valid transformer in its own right. I'm not advocating this be considered "core" in any way. I just want to merge it so that I can use it in parsing.

@jdegoes
Copy link

jdegoes commented May 23, 2014

+1 to that. Having to reason about stack safety is substantial cognitive overhead (which in my mind far eclipses the very slight performance penalty that trampolining has in Javascript).

Really, there's no such thing as FP without monadic recursion (Eff just hides that fact because of inlining and TCO).

I'd much rather people be forced to choose StateT Id if they really know that's what they want (or perhaps more extreme would be trampolining Id, but I know that's too unpopular :) ).

As an aside, that we have to worry about stack safety at all in 2014 is astounding to me. Stack-less VMs are very well-researched. It's a shame they aren't more widespread in mainstream languages.

@paf31 paf31 closed this Jun 3, 2014
@garyb garyb deleted the BounceT branch November 26, 2015 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants